Build Contao backend modules with DCA instead of custom Symfony admin
Use Contao's Data Container Array to get CRUD, sorting, validation and versioning for custom tables from declarative PHP config instead of building a bespoke Symfony admin.
07 Aug 2026, 15:49 UTC

The problem: you need an editorial list, not a full Symfony admin
You need a simple back-end list for FAQ items, press releases or internal contacts. Editors want create, edit, delete, sorting, search and version history. Building a Symfony CRUD controller with forms, validation and a list view is overkill for tabular data that lives in one table.
In Contao 4/5 the practical engineering decision is to model the data as a DCA table. Data Container Array is declarative PHP configuration that describes a database table, its fields, list view and operations. The DC_Table driver interprets the DCA and renders the full back-end UI, so basic CRUD comes without a controller or template.
What DCA gives you for free
A DCA file maps to a database table via the sql key in each field definition. Contao's database update, contao:migrate or the install tool, generates ALTER statements from those definitions, keeping schema and UI in one source of truth.
Listings are configured via list: sorting, label, filters, search and per-record operations like edit, copy, delete and toggle visibility are enabled by configuration, not code. Sorting mode 5 enables a tree.
Input validation and widgets are chosen via inputType with eval options. eval => ['mandatory'=>true,'maxlength'=>255,'tl_class'=>'w50'] is enforced server-side in the back end.
Versioning and undo are inherited. Tables handled by DC_Table get Contao's version history largely for free. That is a practical reason to prefer a DCA table over a bespoke Symfony admin for editorial content.
Minimal working DCA example
Contao 4/5 convention: src/Resources/contao/dca/tl_example.php. Adjust the path for Contao 3.
// src/Resources/contao/dca/tl_example.php
$GLOBALS['TL_DCA']['tl_example'] = [
'config' => [
'dataContainer' => 'Table',
'enableVersioning' => true,
'sql' => ['keys' => ['id' => 'primary']]
],
'list' => [
'sorting' => [
'mode' => 2,
'fields' => ['title'],
'flag' => 1,
'panelLayout' => 'filter,sort,search,limit'
],
'label' => [
'fields' => ['title'],
'format' => '%s'
],
'operations' => [
'edit' => ['href' => 'act=edit','icon' => 'edit.svg'],
'copy' => ['href' => 'act=copy','icon' => 'copy.svg'],
'delete' => ['href' => 'act=delete','icon' => 'delete.svg']
]
],
'palettes' => [
'__selector__' => [],
'default' => '{title_legend},title,description;{publish_legend},published'
],
'fields' => [
'id' => ['sql' => 'int(10) unsigned NOT NULL auto_increment'],
'tstamp' => ['sql' => 'int(10) unsigned NOT NULL default 0'],
'title' => [
'label' => ['Title','Short title'],
'inputType' => 'text',
'eval' => ['mandatory'=>true,'maxlength'=>255,'tl_class'=>'w50'],
'sql' => 'varchar(255) NOT NULL default \'\''
],
'description' => [
'label' => ['Description',''],
'inputType' => 'textarea',
'eval' => ['rte'=>'tinyMCE','tl_class'=>'clr'],
'sql' => 'text NULL'
],
'published' => [
'label' => ['Published',''],
'inputType' => 'checkbox',
'eval' => ['tl_class'=>'w50'],
'sql' => 'char(1) NOT NULL default \'\''
]
]
];
Register the module in back-end navigation via your bundle config or contao/config/config.yml so the list appears. After clearing cache, run the migration as a user with console and DB write access:
php bin/console cache:clear
php bin/console contao:migrate
Expected checks: the tl_example table is created or altered to match the sql definitions, and a new back-end module shows create/edit/delete without a custom controller.
Callbacks let you hook in without replacing the UI. Example: fields.title.save_callback to normalize input, or list.label.label_callback to format the list label. Keep complex business logic in dedicated services and reference them from the DCA to avoid unmaintainable config.
Trade-offs and limits
DCA covers record-based editing well. Highly interactive or non-tabular admin UIs still require custom Symfony controllers and templates.
The automatic schema update only reflects sql definitions. Renaming or dropping a field can destroy data if migrations are applied carelessly. Always back up the database before structural changes.
DCA conventions and default paths differ between Contao 3, 4 and 5. An example written for one major version may not load unchanged in another.
When to use it
Use DCA when the data is tabular, edited by content editors, and benefits from sorting, search, filters and versioning. Use a custom Symfony admin when you need complex interactions, wizards or non-record UIs. For the former, DCA gives you a maintainable, versioned back-end module from declarative config.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.