Define a Custom Database-Backed Content Element in Contao with DCA
Use Contao's Data Container Array to declare a database table, form layout, and list view for a custom content element. The article walks through a full DCA file, a controller with the #[AsContentElement] attribute, migration with contao:migrate, and common pitfalls to avoid.
23 Jun 2026, 05:46 UTC

Why DCA Is the Core of Extension Development
In Contao every back-end editable table is described by a Data Container Array (DCA). The DCA does three things in one place:
- Declares the table schema (via the
sqlkey) - Defines the edit mask (via
palettesandfields) - Configures the list view (via
list)
Because the install tool and the contao:migrate command read the sql definitions, you ship database changes as code, not as hand-written SQL. That is the engineering decision that keeps extensions maintainable and version-control friendly.
Step 1 – Create the DCA File
Place a file in contao/dca/tl_my_element.php. The file must return an array; Contao will merge it into $GLOBALS['TL_DCA'] automatically.
[
'dataContainer' => 'Table',
'enableVersioning' => true,
'sql' => [
'keys' => [
'id' => 'primary',
'alias' => 'unique'
]
]
],
'list' => [
'sorting' => [
'mode' => 4,
'fields' => ['sorting'],
'flag' => 1,
'panelLayout' => 'filter;sort,search,limit'
],
'label' => [
'fields' => ['title', 'alias'],
'showColumns' => true
],
'operations' => [
'edit' => ['href' => 'act=edit', 'icon' => 'edit.svg'],
'delete' => ['href' => 'act=delete', 'icon' => 'delete.svg'],
'toggle' => ['href' => 'act=toggle', 'icon' => 'visible.svg']
]
],
'palettes' => [
'default' => '{title_legend},title,alias,description;{publish_legend},published,start,stop'
],
'fields' => [
'id' => [
'sql' => "int(10) unsigned NOT NULL auto_increment"
],
'tstamp' => [
'sql' => "int(10) unsigned NOT NULL default '0'"
],
'title' => [
'label' => 'Title',
'inputType' => 'text',
'eval' => ['mandatory' => true, 'maxlength' => 255],
'sql' => "varchar(255) NOT NULL default ''"
],
'alias' => [
'label' => 'Alias',
'inputType' => 'text',
'eval' => ['rgxp' => 'alias', 'unique' => true, 'maxlength' => 128],
'sql' => "varchar(128) NOT NULL default ''"
],
'description' => [
'label' => 'Description',
'inputType' => 'textarea',
'eval' => ['rte' => 'tinyMCE'],
'sql' => "text NULL"
],
'published' => [
'label' => 'Published',
'inputType' => 'checkbox',
'eval' => ['doNotCopy' => true],
'sql' => "char(1) NOT NULL default '0'"
],
'start' => [
'label' => 'Start',
'inputType' => 'checkbox',
'eval' => ['tl_class' => 'w50 wizard', 'doNotCopy' => true, 'datepicker' => true],
'sql' => "int(10) unsigned NOT NULL default '0'"
],
'stop' => [
'label' => 'Stop',
'inputType' => 'checkbox',
'eval' => ['tl_class' => 'w50 wizard', 'doNotCopy' => true, 'datepicker' => true],
'sql' => "int(10) unsigned NOT NULL default '0'"
]
]
];
Key points:
- The
sqlstrings are parsed by Contao's migration tool. Keep them simple;int,varchar,textare fine. - Never rename the reserved columns
id,tstamp,sorting,pidunless you also update the DCAconfigaccordingly. - The
listsection controls the back-end overview. Mode 4 creates a tree view that is useful for parent-child relations (e.g. pages).
Step 2 – Register the Content Element
In Contao 5 the modern way is to create a controller class and annotate it with #[AsContentElement]. Older 4.x code can still use the $GLOBALS['TL_CTE'] array, but mixing both will silently fail.
data contains the row from tl_my_element
return $this->render('ce_my_element.html.twig', [
'data' => $this->data
]);
}
}
Place the template in templates/ce_my_element.html.twig and reference the fields you need:
<h2>{{ data.title }}</h2>
<p>{{ data.description|raw }}</p>
After adding the class, run the cache clear command to load the new content element type:
# From the Contao root
php bin/console contao:cache:clear
Step 3 – Ship the Schema with the Install Tool
When your extension is installed, Contao will compare the sql definitions with the live database. If the table tl_my_element does not exist, it will be created automatically. If a column is missing or has the wrong type, the tool will alter it.
# In development or on a new server
php bin/console contao:migrate
Verify the migration by checking the database or by inspecting the back-end list view. The list configuration should now show your rows sorted by sorting and provide edit/delete operations.
Common Pitfalls and How to Avoid Them
- Cache not cleared: After editing a DCA, the back-end will still show the old form until
contao:cache:clearis run. - Using unsupported SQL types: The migration tool only understands
int,varchar,text,char,datetime. For custom types you must write a manual migration. - Renaming reserved columns: Changing
idortstampwithout updating the DCA config breaks Contao's versioning and sorting. - Mixing attribute and legacy registration: In Contao 5, use only the PHP attribute. In Contao 4, register via
$GLOBALS['TL_CTE']. Mixing them results in the element not appearing in the back-end. - Missing
sqlkey: Without it, the install tool cannot create or alter the table. Always include thesqldefinition for every field.
Limitations of the DCA-Based Approach
- Complex migrations (e.g. splitting a column into two) still require hand-written SQL and a custom migration class.
- The automatic migration only supports simple alterations; adding a new column with a default that depends on other columns may fail.
- If you need to expose the data to API consumers, you must write a DataProvider or REST controller yourself; the DCA does not handle that.
Practical Check-List Before Shipping
- Run
php bin/console contao:migratelocally and confirm the table appears. - Open an article in the back-end, add a new
MyElement, fill in the fields, and save. - View the article on the front-end and verify the Twig template renders the data.
- Clear the cache on the target server and repeat the steps to catch caching issues early.
- If the element does not appear in the content element picker, double-check the attribute parameters and the namespace of the controller.
By keeping all database, form, and list logic inside the DCA and using the built-in migration tool, you make your extension portable, version-control friendly, and easy to maintain.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.