Create a custom Contao module – part three (last)

In the previous part of the tutorial we have created the Data Container Array for our module. Now when we got our backend section ready, we can create a frontend module that will display our collection on the website. We already have got C, U and D out of CRUD – now it’s time to develop the Read section.
Click here to read this tutorial in italian!
Before we start, I want to remind you that the frontend section of module usually consists of php files in the module’s root directory and .tpl files in the /templates directory.
The TYPOlight isn’t bulid upon a typical MVC framework, as the controller and model are mixed together. So we can say the php files in the /cd_collection are sort of hybrid. They are used to retreive data from the database, parse it and pass to the templates, which are stored in the /templates directory.

In this tutorial we will develop only a simple cds listing. It isn’t going to be divided into Reader and Listing modules (like News). However, after you finish reading this, you should be able to extend our module into those two parts.
Okay, let’s get to work.
File structure
First of all, create the two files:
- /ModuleCdCollection.php (in the module’s root directory)
- /templates/mod_cdcollection.tpl
There is nothing to describe here, so let’s write some code.
ModuleCdCollection.php
In the first part, we defined in config.php, that our frontend module will use ModuleCdCollection class:
// Front end module array_insert($GLOBALS['FE_MOD']['miscellaneous'], 0, array ( 'cd_collection' => 'ModuleCdCollection' ));
It is a good convention to name a file the way file_name==class_name. You might know this from various php frameworks.
Now open up the ModuleCdCollection.php file and put the following code:
<?php
class ModuleCdCollection extends Module
{
protected $strTemplate = 'mod_cdcollection';
protected function compile()
{
//
}
}
?>
As you can see we have created the ModuleCdCollection class that extends Module. The Module class provides two unique functions and one unique variable. The protected variable $strTemplate defines a template file that will be used by module. Notice that we do not include file path, as Contao takes module_root/templates as a default one. I hope you suppose that Contao will look for our template file in path templates/mod_cdcollection.tpl.
The two unique functions for Module are compile() and generate(). Those could be divided into frontend – compile() and backend – generate(). We actually don’t need the generate function as we want to display collection only in the frontend (Contao can display a module in the backend using DCA).
Now, it is time for the most interesting part of module development – the actual coding:
protected function compile()
{
$intCategory = ($this->Input->get('cat')) ? $this->Input->get('cat') : 1;
$arrCds = array();
$objCds = $this->Database->prepare("SELECT * FROM tl_cds WHERE pid=? ORDER BY title")->execute($intCategory);
$objCategory = $this->Database->prepare("SELECT * FROM tl_cds_category WHERE id=?")->execute($intCategory);
$objCategories = $this->Database->execute("SELECT id, title FROM tl_cds_category ORDER BY title");
while ($objCds->next())
{
$arrCds[] = array
(
'title' => $objCds->title,
'artist' => $objCds->artist,
'comment' => $objCds->comment,
'src' => $objCds->image,
'alt' => $objCds->title
);
}
while ($objCategories->next())
{
$blnSelected = ($objCategories->id == $intCategory) ? true : false;
$arrCategories[] = array
(
'id' => $objCategories->id,
'title' => $objCategories->title,
'selected' => $blnSelected
);
}
$this->Template->cds = $arrCds;
$this->Template->categories = $arrCategories;
$this->Template->catTitle = $objCategory->title;
$this->Template->catDescription = $objCategory->description;
}
Let’s see what we have done here. Firstly, we retrieve the $_GET['cat'] variable, which will tell our module what category should be displayed. If this variable is not set, automatically display the first added category (id = 1).
Next, we execute three queries to the database: cds list of current category, current category data, full list of categories. The full list of categories will be used to create a dropdown menu, which allows visitor to switch between categories.
In the first while() loop, we get all the cds and put them into array. $objCds->next() makes sure none of the records is omitted.
In the second loop, we do the same thing as before, but with categories. We also flag which of the dropdown menu options is currently active. Usability +1.
Finally, we assign all the data to the template.
templates/mod_cdcollection.tpl
Now as we got a controller of our module, it is time to create a view. Open the templates/mod_cdcollection.tpl file and put the following code:
<span>Choose the category:</span>
<form action="" method="GET">
<select name="cat">
<?php foreach($this->categories as $category): ?>
<option value="<?php echo $category['id']; ?>"<?php if ($category['selected']): ?> selected="selected"<?php endif; ?>><?php echo $category['title']; ?></option>
<?php endforeach; ?>
</select>
<input type="submit" value="OK" />
</form>
<h3><?php echo $this->catTitle; ?></h3>
<?php echo $this->catDescription; ?>
<table border="0" cellpadding="4">
<?php foreach ($this->cds as $cd): ?>
<tr>
<td><img src="<?php echo $cd['src']; ?>" height="100" width="100" alt="<?php echo $cd['alt']; ?>" /></td>
<td>
<p><strong><?php echo $cd['title']; ?></strong> (<?php echo $cd['artist']; ?>)</p>
<?php echo $cd['comment']; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
Well, this code might look unreadable, so I recommend to paste it into your favourite editor.
Firstly, we have made a form that provides a dropdown menu with the categories. As the option’s value we take category’s id, then we make it selected by default if it’s an active category.
Next, we display the category’s title and description.
We create a table that will holds our cds. Then comes a foreach() loop that goes through our array of cds. I hope this code is clear for you. You could also put this code to check how the array looks like:
<pre><?php print_r($this->cds); ?></pre>
Try it out now. It should display something similar to this:

Labels
Although our module is working, we should make it more user friendly in the backend. What I mean is changing the e.g. {title_legend} to “CD title”.
Create a new directory “en” in the /languages folder. Then create three files called tl_cds.php, tl_cds_category.php and modules.php.
Notice that the naming convention is the same as in /dca folder.
Okay, open up the languages/en/tl_cds_category.php and put the following code:
<?php
/**
* Fields
*/
$GLOBALS['TL_LANG']['tl_cds_category']['title'] = array('Title', 'Please enter the title of the category.'); // 1
$GLOBALS['TL_LANG']['tl_cds_category']['description'] = array('Description', 'Please enter the description of the category.'); // 2
/**
* Legends
*/
$GLOBALS['TL_LANG']['tl_cds_category']['title_legend'] = 'Title and description'; // 3
/**
* Buttons
*/
$GLOBALS['TL_LANG']['tl_cds_category']['new'] = array('New category', 'Create a new category'); // 4
$GLOBALS['TL_LANG']['tl_cds_category']['show'] = array('Category details', 'Show details of category ID %s'); // 5
$GLOBALS['TL_LANG']['tl_cds_category']['edit'] = array('Edit category', 'Edit category ID %s'); // 6
$GLOBALS['TL_LANG']['tl_cds_category']['copy'] = array('Copy category', 'Duplicate category ID %s'); // 7
$GLOBALS['TL_LANG']['tl_cds_category']['delete'] = array('Delete category', 'Delete category ID %s'); // 8
?>
I have prepared a screenshot to explain better what line describes what:

Now a little explanation where to get the labels’ variables from.
Fields are taken directly from the dca file:
// Fields
'fields' => array
(
'title' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_cds_category']['title'],
'inputType' => 'text',
'search' => true,
'eval' => array('mandatory'=>true, 'maxlength'=>64)
),
// ...
Field’s label is an array, which first element contains the field’s title, while the second one contains the description (also known as explanation) of the field.
Legends are taken from the “palettes” array:
// Palettes
'palettes' => array
(
'default' => '{title_legend},title,description'
),
Buttons are taken from “opreations” array. However, notice that there is one additional label:
$GLOBALS['TL_LANG']['tl_cds_category']['new'] = array('New category', 'Create a new category');
This button is not defined in the dca array, and is visible unless the table config’s array has got the “closed” property set to true 'closed' => true.
That’s all of the labels theory for now. Let’s fill the tl_cds.php file:
<?php
/**
* Fields
*/
$GLOBALS['TL_LANG']['tl_cds']['title'] = array('CD Title', 'Please enter the title of the CD.');
$GLOBALS['TL_LANG']['tl_cds']['artist'] = array('Artist', 'Please enter the author of the CD.');
$GLOBALS['TL_LANG']['tl_cds']['image'] = array('Cover image', 'Please choose the CD cover image.');
$GLOBALS['TL_LANG']['tl_cds']['comment'] = array('Comment', 'Please enter your comment about CD.');
/**
* Legends
*/
$GLOBALS['TL_LANG']['tl_cds']['title_legend'] = 'Title and artist';
$GLOBALS['TL_LANG']['tl_cds']['image_legend'] = 'CD Cover';
$GLOBALS['TL_LANG']['tl_cds']['comment_legend'] = 'Comment';
/**
* Buttons
*/
$GLOBALS['TL_LANG']['tl_cds']['new'] = array('New CD', 'Create a new CD');
$GLOBALS['TL_LANG']['tl_cds']['show'] = array('CD details', 'Show details of CD ID %s');
$GLOBALS['TL_LANG']['tl_cds']['edit'] = array('Edit CD', 'Edit CD ID %s');
$GLOBALS['TL_LANG']['tl_cds']['copy'] = array('Copy CD', 'Duplicate CD ID %s');
$GLOBALS['TL_LANG']['tl_cds']['delete'] = array('Delete CD', 'Delete CD ID %s');
?>
The above code is very similar to the previous one. I think it doesn’t need any additional explanation.
Okay, we already got the “internal” labels ready. Now we should create few other labels that are displayed in miscellaneous sections of Contao.
Open the languages/en/modules.php file and put the following content:
<?php
/**
* Back end modules
*/
$GLOBALS['TL_LANG']['MOD']['cd_collection'] = array('CD Collection', 'Manage your CD collection.');
/**
* Front end modules
*/
$GLOBALS['TL_LANG']['FMD']['cd_collection'] = array('CD Collection list', 'adds a list of cds to your page.');
?>
Instead of explaining what they affect, I will simply show you on the image:

Securing files
As the last point, I want to show you how to simply secure our files that they can’t be accessed directly. This method is used in (almost) all php files in Contao.
Now open up all the php files of our module and replace the <?php tag with:
<?php if (!defined('TL_ROOT')) die('You can not access this file directly!');
Final notes
Congratulations! You have just developed your own Contao module! I hope none part of this tutorial was difficult for you, as I explained everything as much as I could. Thank you very much for your attention. Till next time.
Download files
Download all files included in this tutorial – cd_collection.zip (~ 12kB).
About Author
berger June 14th
thanks, helped a lot!
Murph June 29th
awesome description!
Tien nguyen August 9th
Great tutorial, thank you.
Stefan September 20th
Hey!
Very helpfull tut, thanks a bundle!
But I noticed some problem – when debugging-output is activated, I get this in the CD-Collection-List:
Warning: Illegal offset type in /Library/WebServer/Documents/contao-2.9.1/system/drivers/DC_Table.php on line 3428
#0 /Library/WebServer/Documents/contao-2.9.1/system/drivers/DC_Table.php(3428): __error(2, ‘Illegal offset …’, ‘/Library/WebSer…’, 3428, Array)
#1 /Library/WebServer/Documents/contao-2.9.1/system/drivers/DC_Table.php(345): DC_Table->parentView()
#2 /Library/WebServer/Documents/contao-2.9.1/system/modules/backend/Backend.php(234): DC_Table->showAll()
#3 /Library/WebServer/Documents/contao-2.9.1/contao/main.php(101): Backend->getBackendModule(‘cd_collection’)
#4 /Library/WebServer/Documents/contao-2.9.1/contao/main.php(304): Main->run()
#5 {main}
This is with the unaltered ZIP from this site and contao 2.9.1.
The Module still works, but the debugging output is kinda ugly
Especially since it gets longer with the number of CDs…
Any clue?
Kamil Kuzminski September 20th
@Stefan
You are very right. Since Contao 2.9 the backend’s mode view 4 was changed. To fix this issue, go to cd_collection/dca/tl_cds.php file and modify the $GLOBALS['TL_DCA']['tl_cds']['list']['sorting'] array by adding:
'flag' => 1, 'fields' => array('title'),so it looks like
'sorting' => array ( 'mode' => 4, 'flag' => 1, 'fields' => array('title'), 'headerFields' => array('title', 'description'), 'panelLayout' => 'search,limit', 'child_record_callback' => array('tl_cds', 'listCds') ),I will try to update the tutorial in my spare time.
Qualtext September 20th
there is a little mistake above:
“Okay, open up the languages/en/tl_cds.php and put the following code:”
it isn’t the tl_cds.php but tl_cds_category.php
Qualtext September 20th
after programming i’ve got some errors. so i downloaded the cd_collection.zip an install it.
so errors shown.
now i create a new article, but in selction-list is no option “cd_collection”.
i use contao (2.9) in german version.
how can i fix it?
thank you so much for this nice tutorial!
Kamil Kuzminski September 21st
@Qualtext
The mistake is corrected now. And according to errors, I’m going to review this tutorial once again, so it becomes compatible with Contao 2.9 too.
Jacek October 22nd
Very good tutorial. Is it possible that you posted a brief description of how to add additional fields in the section of the module, such as how to add a field to select a template for this module. Or how to add CSS class field.
I would really appreciate it.
bOrste October 29th
Fantastic Tutorial!
bOrste October 29th
i again,
i have made your tutorial, the frontend module part works fine with database, but if i click in the backend on the module link in the navigation, i got a “blank white page”. if i active error report that i get following errors:
Fatal error: Could not create a data container object in /var/www/html/system/modules/backend/Backend.php on line 167
#0 /var/www/html/system/modules/backend/Backend.php(167): __error(256, ‘Could not creat…’, ‘/var/www/web327…’, 167, Array)
#1 /var/www/html/contao/main.php(101): Backend->getBackendModule(‘Could not creat…’, 256)
#2 /var/www/html/contao/main.php(304): Main->run(‘provider’)
#3 {main}
Notice: Undefined index: ORIG_PATH_INFO in /var/www/html/system/libraries/Environment.php on line 139
Notice: Undefined index: PATH_INFO in /var/www/html/system/libraries/Environment.php on line 139
Notice: Undefined index: ORIG_SCRIPT_NAME in /var/www/html/system/libraries/Environment.php on line 139
Notice: Undefined index: key in /var/www/html/system/libraries/Input.php on line 95
Notice: Undefined index: token in /var/www/html/system/libraries/Input.php on line 9
do you how i can solve the problem?
thank you
PS: i have contao version 2.9.1
Kamil Kuzminski October 30th
Make sure you haven’t misspelled the
bOrste October 30th
sry, it was my mistake. i copied ‘config’ two times ..
last but no least i have a question at the database.sql.
must a parent id exactly called “pid” ?
and is the “tstmp” attribute in every table essential?
ty
Kamil Kuzminski October 30th
Yes, parent id must be called “pid”, that is required by Contao. I don’t know whether “tstamp” needs to be present, though, it is present in all Contao modules.
Here is a list of fields that you need to have in order to make your module use all of the features that are provided by Contao:
id, pid, sorting*, tstamp.
* – mainly used in sorting mode 4
KarlTietze November 22nd
Thank you for that great Tutorial!
I have a question (pretty new to Contao am I)….
I need a way to display a single CD with its details from the CD Collection on one page.
So i assume i need to create a ContentElement that displays a form in the BE. In that form i select a CD from the CD Collection.
Now the details are fetched from the DB and in the FE a nice page is rendered
But how to do that???
I am not too PHP savy (i am more from the ASP.NET side…)
Any tips?
Thank you in advance,
Karl
Kamil Kuzminski November 22nd
@KarlTietze
http://www.contao-community.org/viewtopic.php?f=9&t=2411
KarlTietze November 22nd
muchas gracias, mister Tru.
I will follow your advice, master
Kamil Kuzminski November 22nd
Just pointing other readers to the forum topic where more things can be discussed and explained, than here in comments
Andreas April 17th
Hello Kamil, thank you. Very goog work. I’m a block hater, but like to read all of your blocks. I’m missing some .htaccess files in your extension, that’s all. I would like to read another block by you about creating an extension for a new content element with individual templates. Bye Andreas
iop September 28th
Hey Kamil,
first of all I want to thank you for this excelent tutorial! But I have a problem saving CDs in a category – I can fill out all fields, push the save button, but after returning to my category, all cds are gone… My Contao version is 2.10.1. Can someone help me, a newbie in contao module developing, please?
Kamil Kuzminski September 28th
@iop
This is probably because something is wrong with pid field or ptable/ctable values. Every time you switch to CDs view, Contao checks for all records that “pid” value is 0 and deletes them.
patrick Beck October 3rd
thank you very much, your tutorial is great!
Nhan November 28th
thank you,
Joc December 19th
Hi from Germay! Thanks a lot. Your tutorial for building own modules is the clearest and most pragmatic one I found on the web. (And i searched a lot). Sad that you only continue your website in polish. I think you give very inspirating inputs for interested developers. best reagrds!
Nico December 20th
Hi from France,
nothing appears in the front office ?
do you know why .
i have made a second file fr like en but nothing.
The backoffice is ok (except for upload an image)
Nico
Christian February 29th
Hello Kamil,
thank you for this great tutorial. Helped a lot and is still up to date for Contao 2.11
Uwe March 8th
Thanks for this great tutorial!!
Nils April 3rd
This is what a tutorial should be like! Awesome! Thank you so much!
Add Yours
YOU