In this tutorial we will have a look on how to build a DCA definition, so you can manage custom data records within the Contao back end for your web application project. We will devise an example utilising different features and then build each aspect step by step.
In our example we will assume that we are creating a web application for a wholesale dealer who is trading “industrial parts”. Each part belongs to a vendor and can have detailed information like the name, part number, description and an image. Furthermore each vendor can have detailed information as well, like the name of the vendor and their main address.
For the back end workflow we decide that the “parts” should be organised as child records for each vendor and that we will manage these records under the main menu item called “Parts” within the other “Content” menu items.
For the actual data record tables we decide on the table names tl_vendor
(for
the vendor records) and tl_parts
(for the parts records).
Creating the DCA in this case is divided into two main steps: creating
the DCA for our tl_vendor
records and creating the DCA for our tl_parts
records.
For each table, we will go through the individual steps of defining the configuration,
record listing, fields, palettes and may be callbacks.
tl_vendor
To create our DCA for the tl_vendor
table, first create the following file
within your Contao installation: /contao/dca/tl_vendor.php
(Note: prior to Contao
4.8 these files need to be created under /app/Resources/contao/dca/tl_vendor.php
).
For our DCA configuration of tl_vendor
, we want to define the following:
tl_parts
table.id
for our records.// contao/dca/tl_vendor.php
use Contao\DC_Table;
$GLOBALS['TL_DCA']['tl_vendor'] = [
'config' => [
'dataContainer' => DC_Table::class,
'ctable' => ['tl_parts'],
'enableVersioning' => true,
'switchToEdit' => true,
'sql' => [
'keys' => [
'id' => 'primary',
'tstamp' => 'index',
],
],
],
];
Defining the child record table is important so that we can then later actually edit the child records for each vendor record.
Here is where things get more complex. We want:
This results in the following configuration for the list
part of the DCA:
// contao/dca/tl_vendor.php
use Contao\DataContainer;
$GLOBALS['TL_DCA']['tl_vendor'] = [
'config' => […],
'list' => [
'sorting' => [
'mode' => DataContainer::MODE_SORTED,
'fields' => ['name'],
'flag' => DataContainer::SORT_INITIAL_LETTER_ASC,
'panelLayout' => 'search,limit'
],
'label' => [
'fields' => ['name'],
'format' => '%s',
],
'operations' => [
'edit' => [
'href' => 'table=tl_parts',
'icon' => 'edit.svg',
],
'editheader' => [
'href' => 'act=edit',
'icon' => 'header.svg',
],
'delete' => [
'href' => 'act=delete',
'icon' => 'delete.svg',
],
'show' => [
'href' => 'act=show',
'icon' => 'show.svg'
],
],
],
];
As you can see we have defined the sorting of all records and the label and operations for each record.
Now we will actually define the fields for our tl_vendor
records. As previously
defined, each vendor should have a name and an address. We will further split the
address into street, postal, city and country.
// contao/dca/tl_vendor.php
$GLOBALS['TL_DCA']['tl_vendor'] = [
'config' => […],
'list' => […],
'fields' => [
'id' => [
'sql' => ['type' => 'integer', 'unsigned' => true, 'autoincrement' => true],
],
'tstamp' => [
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0]
],
'name' => [
'search' => true,
'inputType' => 'text',
'eval' => ['tl_class' => 'w50', 'maxlength' => 255, 'mandatory' => true],
'sql' => ['type' => 'string', 'length' => 255, 'default' => '']
],
'street' => [
'inputType' => 'text',
'eval' => ['tl_class' => 'w50', 'maxlength' => 255, 'mandatory' => true],
'sql' => ['type' => 'string', 'length' => 255, 'default' => '']
],
'postal' => [
'inputType' => 'text',
'eval' => ['tl_class' => 'clr w50', 'maxlength' => 255, 'mandatory' => true],
'sql' => ['type' => 'string', 'length' => 255, 'default' => '']
],
'city' => [
'inputType' => 'text',
'eval' => ['tl_class' => 'w50', 'maxlength' => 255, 'mandatory' => true],
'sql' => ['type' => 'string', 'length' => 255, 'default' => '']
],
'country' => [
'inputType' => 'select',
'options_callback' => static function(): array {
return \Contao\System::getContainer()->get('contao.intl.countries')->getCountries();
},
'eval' => ['tl_class' => 'w50', 'mandatory' => true, 'includeBlankOption' => true],
'sql' => ['type' => 'string', 'length' => 2, 'default' => '']
],
],
];
When table data records are managed by Contao, you always need to add at least
the fields id
and tstamp
. The latter will contain a Unix timestamp when the
record was last edited.
For the other fields have a look at the fields reference to see all the possibilities
for each field. For the country
selection field we also used another feature:
we are retrieving all countries directly from the system via the contao.intl.countries
service.
This gives us an associative array of all translated countries, indexed by their
country code (e.g. 'at' => 'Austria'
).
If your parent table has a field called either title
or name
, Contao will automatically
add the title or name of the parent element to the back end’s main headline breadcrumb,
when viewing or editing the child records of that parent.
When editing a record the layout of the input fields is defined by a “palette”. For our vendor records we only need to provide a single default palette. However, within that palette, we will group the address information and thus separate it from the vendor’s name.
// contao/dca/tl_vendor.php
$GLOBALS['TL_DCA']['tl_vendor'] = [
'config' => […],
'list' => […],
'fields' => […],
'palettes' => [
'default' => '{vendor_legend},name;{address_legend},street,postal,city,country'
],
];
This finishes our DCA definition for tl_vendor
.
tl_parts
Now we will execute the same steps for our tl_parts
table. Only the major differences
will be outlined.
The basic DCA configuration is the same as before. This time we do not have a child
table though, but we must define a parent table. Also we are using an onload_callback
here to dynamically adjust our DCA definition. We are assuming that the part number
always consists of the first two letters of the vendor. So to make things easier
for the editor, we fetch the name of the parent (the vendor) and define the first
two letters of the name as the default for the number
field.
// contao/dca/tl_parts.php
use Contao\Database;
use Contao\DC_Table;
use Contao\Input;
$GLOBALS['TL_DCA']['tl_parts'] = [
'config' => [
'dataContainer' => DC_Table::class,
'enableVersioning' => true,
'ptable' => 'tl_vendor',
'sql' => [
'keys' => [
'id' => 'primary',
'tstamp' => 'index',
],
],
'onload_callback' => [
function () {
$db = Database::getInstance();
$pid = Input::get('pid');
if (empty($pid)) {
return;
}
$result = $db->prepare('SELECT `name` FROM `tl_vendor` WHERE `id` = ?')
->execute([$pid]);
$prefix = strtoupper(substr($result->name, 0, 2));
$GLOBALS['TL_DCA']['tl_parts']['fields']['number']['default'] = $prefix;
},
]
],
];
Generally it is recommended to use services for such callbacks. For the simplicity of this article an anonymous function is implemented, using the legacy way of retrieving the database connection and parameter inputs.
For the list configuration we are using sorting mode 4
, which is specific to
displaying child records of a parent record. In this mode however we need to define
a child_record_callback
, otherwise no records will be rendered. this callback
is implemented as an anonymous function.
// contao/dca/tl_parts.php
$GLOBALS['TL_DCA']['tl_parts'] = [
'config' => […],
'list' => [
'sorting' => [
'mode' => 4,
'fields' => ['name'],
'headerFields' => ['name'],
'panelLayout' => 'search,limit',
'child_record_callback' => function (array $row) {
return '<div class="tl_content_left">'.$row['name'].' ['.$row['number'].']</div>';
},
],
'operations' => [
'edit' => [
'href' => 'act=edit',
'icon' => 'edit.svg',
],
'delete' => [
'href' => 'act=delete',
'icon' => 'delete.svg',
],
'show' => [
'href' => 'act=show',
'icon' => 'show.svg'
],
],
],
];
As described before, our parts will consist of a name, number, description and an image. For our description we will provide the ability to use the TinyMCE editor. For the image we will configure the ability to use the file tree picker.
We also defined the flag 1
for our name
field. Using list sorting mode 4
Contao
will group the child records according to the given fields. By setting the flag
of the field to 1
, the child records will be grouped by their name’s initial letter.
// contao/dca/tl_parts.php
$GLOBALS['TL_DCA']['tl_parts'] = [
'config' => […],
'list' => […],
'fields' => [
'id' => [
'sql' => ['type' => 'integer', 'unsigned' => true, 'autoincrement' => true],
],
'pid' => [
'foreignKey' => 'tl_vendor.name',
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
'relation' => ['type'=>'belongsTo', 'load'=>'lazy']
],
'tstamp' => [
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0]
],
'name' => [
'search' => true,
'flag' => 1,
'inputType' => 'text',
'eval' => ['tl_class' => 'w50', 'maxlength' => 255, 'mandatory' => true],
'sql' => ['type' => 'string', 'length' => 255, 'default' => '']
],
'number' => [
'search' => true,
'inputType' => 'text',
'eval' => ['tl_class' => 'w50', 'maxlength' => 255, 'mandatory' => true],
'sql' => ['type' => 'string', 'length' => 255, 'default' => '']
],
'description' => [
'inputType' => 'textarea',
'eval' => ['tl_class' => 'clr', 'rte' => 'tinyMCE', 'mandatory' => true],
'sql' => ['type' => 'text', 'notnull' => false]
],
'singleSRC' => [
'inputType' => 'fileTree',
'eval' => [
'tl_class' => 'clr',
'fieldType' => 'radio',
'filesOnly' => true,
'extensions' => \Contao\Config::get('validImageTypes'),
'mandatory' => true,
],
'sql' => ['type' => 'binary', 'length' => 16, 'notnull' => false, 'fixed' => true]
],
],
];
The only thing left is the palette definition. We only need the default palette and we will use only one group.
// contao/dca/tl_parts.php
$GLOBALS['TL_DCA']['tl_parts'] = [
'config' => […],
'list' => […],
'fields' => […],
'palettes' => [
'default' => '{parts_legend},name,number,description,singleSRC'
],
];
This finishes our DCA definition for tl_parts
.
So far there will not be a back end menu item yet where the records can be managed,
because the back end module definition is still missing. This is done in the /contao/config/config.php
file using the $GLOBALS['BE_MOD']
global array. It is an associative array containing
all the back end modules grouped in their context (e.g. content
).
For our back end module to appear as the last item in the main Content menu, we need to do the following:
// contao/config/config.php
$GLOBALS['BE_MOD']['content']['parts'] = [
'tables' => ['tl_vendor', 'tl_parts'],
];
The definition depends on the type of back end module. Since in our case we are managing table records, we have to define the tables to be managed.
Now that we have finished the DCA configuration and added our back end module, we can actually start creating and managing records. However, things will look a bit off since our back end module is missing translations in many places. For example labels for creating records and palette legends need to be defined for each data container.
Translations must be put into the /contao/languages
folder, while each language
will have its own subfolder there. Our translations will go into the /contao/languages/en
folder.
The English translations will also be the fallback when there are no other translations available.
First we will add the translation for our back end module, which goes into the modules.php
:
// contao/languages/en/modules.php
$GLOBALS['TL_LANG']['MOD']['parts'] = ['Parts', 'Manage vendors and parts.'];
$GLOBALS['TL_LANG']['MOD']['tl_vendor'] = 'Vendors';
$GLOBALS['TL_LANG']['MOD']['tl_parts'] = 'Parts';
All other translations will go into files that have the same name as our data container table names. Contao 4.9 and up will
automatically pull these translations for our fields if the translation key is the same as the field’s key. There are also special
translation keys like new
which are used for the button that create a new data record.
// contao/languages/en/tl_vendor.php
$GLOBALS['TL_LANG']['tl_vendor']['new'] = ['Create new vendor', 'Creates a new vendor.'];
$GLOBALS['TL_LANG']['tl_vendor']['edit'] = ['Edit vendor ID %s', 'Edit vendor ID %s'];
$GLOBALS['TL_LANG']['tl_vendor']['vendor_legend'] = 'Vendor';
$GLOBALS['TL_LANG']['tl_vendor']['address_legend'] = 'Address';
$GLOBALS['TL_LANG']['tl_vendor']['name'] = ['Name', 'Name of the vendor.'];
$GLOBALS['TL_LANG']['tl_vendor']['street'] = ['Street', "Street part of the vendor's address."];
$GLOBALS['TL_LANG']['tl_vendor']['postal'] = ['Postal code', "Postal code of the vendor's address."];
$GLOBALS['TL_LANG']['tl_vendor']['city'] = ['City', "City of the vendor's address."];
$GLOBALS['TL_LANG']['tl_vendor']['country'] = ['Country', "Country of the vendor's address."];
// contao/languages/en/tl_parts.php
$GLOBALS['TL_LANG']['tl_parts']['new'] = ['Create new part', 'Creates a new part.'];
$GLOBALS['TL_LANG']['tl_parts']['edit'] = ['Edit part ID %s', 'Edit part ID %s'];
$GLOBALS['TL_LANG']['tl_parts']['parts_legend'] = 'Part';
$GLOBALS['TL_LANG']['tl_parts']['name'] = ['Name', 'Name of the part.'];
$GLOBALS['TL_LANG']['tl_parts']['number'] = ['Number', 'Part number.'];
$GLOBALS['TL_LANG']['tl_parts']['description'] = ['Description', 'Description of the part.'];
$GLOBALS['TL_LANG']['tl_parts']['singleSRC'] = ['Image', 'Image of the part.'];