Callbacks
Callbacks are entry-points for custom code in the DCA. Using callbacks you
can modify the static Data Container Array during runtime.
Callback functions are based on the event dispatcher pattern. They are similar
to Hooks, but always bound to a specific DCA table. You can register
one or more callbacks for a certain event and when the event is triggered, the
callback functions are being executed. See the framework article
on how to register callbacks.
The following is a reference of all available callbacks, using their service tag
callback property name.
Generally these callbacks are executed in the back end, e.g. when editing data records.
However in some instances they might also be executed by front end modules, most
prominently the member modules. In this case the parameters passed to the callback
will be different, as there will be no \Contao\DataContainer
instance for example,
which only exists in the back end. The reference below will list these differences
of the respective callbacks. Keep in mind that any extension might also execute
any of these callbacks in the front end.
Global Callbacks
config.onload
Executed when the DataContainer object is initialized. Allows you to e.g. check
permissions or to modify the Data Container Array dynamically at runtime.
Parameters
Back end
\Contao\DataContainer
Data Container object or null
return: void
Front end modules “Personal data”, “Registration”, “Password” & “Change Password”
No parameters.
return: void
Example
This example changes the mandatory
attribute for the tl_content.text
field for a specific content element.
// src/EventListener/DataContainer/MakeTextNotMandatoryCallback.php
namespace App\EventListener\DataContainer;
use Contao\ContentModel;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use Symfony\Component\HttpFoundation\RequestStack;
#[AsCallback(table: 'tl_content', target: 'config.onload')]
class MakeTextNotMandatoryCallback
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function __invoke(DataContainer $dc = null): void
{
if (null === $dc || !$dc->id || 'edit' !== $this->requestStack->getCurrentRequest()->query->get('act')) {
return;
}
$element = ContentModel::findById($dc->id);
if (null === $element || 'my_content_element' !== $element->type) {
return;
}
$GLOBALS['TL_DCA']['tl_content']['fields']['text']['eval']['mandatory'] = false;
}
}
config.oncreate
Executed when a new record is created.
Parameters
string
Tableinteger
Insert IDarray
Fields of the new record\Contao\DataContainer
Data Container object
return: void
config.onbeforesubmit
This feature is available in Contao 5.0 and later.
Executed when a back end form is submitted before the record will be updated
in the database. Allows you to e.g. modify the values or introduce validation
accross multiple fields. You are expected to return the values.
Parameters
array
Values of the record\Contao\DataContainer
Data Container object
return: array
Values of the record
config.onsubmit
Executed when a back end form is submitted after the record has been updated
in the database. Allows you to e.g. modify the record afterwards (used to calculate
intervals in the calendar extension).
Parameters
Back end
\Contao\DataContainer
Data Container object
return: void
Front end module “Personal data”
\Contao\FrontendUser
The front end user instance\Contao\ModulePersonalData
The front end module instance
return: void
Example
// src/EventListener/DataContainer/NewsSubmitCallbackListener.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use Doctrine\DBAL\Connection;
#[AsCallback(table: 'tl_news', target: 'config.onsubmit')]
class NewsSubmitCallbackListener
{
private $db;
public function __construct(Connection $db)
{
$this->db = $db;
}
public function __invoke(DataContainer $dc): void
{
if (!$dc->id) {
return;
}
$this->db->update('tl_news', ['foobar' => 'foo'], ['id' => $dc->id]);
}
}
config.ondelete
Executed before a record is removed from the database.
Parameters
DC_Folder
(e.g. tl_files
)
string
The path of the file\Contao\DataContainer
Data Container object
return: void
Other Data Containers
\Contao\DataContainer
Data Container objectinteger
The ID of the tl_undo
database record
return: void
Example
This example also removes a record from a different table, if a front end member
is deleted in the back end.
// src/EventListener/DataContainer/MemberDeleteCallbackListener.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use Doctrine\DBAL\Connection;
#[AsCallback(table: 'tl_member', target: 'config.ondelete')]
class MemberDeleteCallbackListener
{
private $db;
public function __construct(Connection $db)
{
$this->db = $db;
}
public function __invoke(DataContainer $dc, int $undoId): void
{
if (!$dc->id) {
return;
}
$this->db->delete('tl_foobar', ['member' => (int) $dc->id]);
}
}
config.oncut
Is executed after a record has been moved to a new position.
Parameters
\Contao\DataContainer
Data Container object
return: void
config.oncopy
Executed after a record has been duplicated.
Parameters
integer
Insert ID\Contao\DataContainer
Data Container object
return: void
config.oncreate_version
Executed after the old version of the record has been added to tl_version
.
Parameters
string
Tableinteger
Parent ID of the tl_version
entryinteger
Version numberarray
Record data
return: void
config.onrestore
Executed after a record has been restored from an old version.
Parameters
integer
Parent ID of the tl_version
entrystring
Tablearray
Record datainteger
Version number
return: void
config.onrestore_version
Executed after a record has been restored from an old version.
Parameters
string
Tableinteger
Parent ID of the tl_version
entryinteger
Version numberarray
Record data
return: void
config.onundo
Executed after a deleted record has been restored from the “undo” table.
Parameters
string
Tablearray
Record data\Contao\DataContainer
Data Container object
return: void
This feature is available in Contao 4.7 and later.
This callback is executed whenever a record is changed in any way via the Contao
back end. It allows you to add additional cache tags that should be invalidated.
Parameters
\Contao\DataContainer
Data Container objectarray
Tags
return: array
An array of cache tags to be invalidated
config.onshow
This feature is available in Contao 4.7 and later.
Allows you to customize the info modal window
of a database record.
Parameters
array
Existing modal window dataarray
Record data\Contao\DataContainer
Data Container object
return: array
An array containing the table rows and columns for the modal
window.
config.onpalette
This feature is available in Contao 5.3 and later.
Allows to dynamically adjust the palette. This can also be achieved using e.g. the config.onload
callback where you
can modify the palette as it is a global variable. However, adjusting it depending on the object’s values is way easier
using config.onpalette
making it automatically work for e.g. the edit multiple mode.
Parameters
string
The current palette\Contao\DataContainer
Data Container object
return: string
The adjusted palette
Example
// src/EventListener/DataContainer/PagePaletteCallback.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DataContainer\PaletteManipulator;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
#[AsCallback(table: 'tl_page', target: 'config.onpalette')]
class PagePaletteCallback
{
public function __invoke(string $palette, DataContainer $dc): string
{
$currentRecord = $dc->getCurrentRecord();
// This shouldn't happen, defensive programming
if (null === $currentRecord) {
return $palette;
}
// Adjust palettes for root pages
if ('root' === $currentRecord['type']) {
$palette = PaletteManipulator::create()
->addLegend('my_legend')
->addField(['my_field_one', 'my_field_two'], 'my_legend', PaletteManipulator::POSITION_APPEND)
->applyToString($palette)
;
}
return $palette;
}
}
Listing Callbacks
All listing callbacks are singular callbacks - meaning there can only be one
callback, not multiple ones.
Allows for individual paste buttons and is e.g. used in the site structure to
disable buttons depending on the user’s permissions (requires an additional
command check via load_callback).
Parameters
\Contao\DataContainer
Data Container objectarray
Record datastring
Tablebool
Whether this is a circular reference of the tree viewarray
Clipboard dataarray
/null
Childrenstring
/null
“Previous” labelstring
/null
“Next” label
return: string
HTML for additional buttons
list.sorting.child_record
Defines how child elements are rendered in “parent view”.
Parameters
return: string
HTML for the child record
Allows for individual labels in header of “parent view”.
Parameters
array
Current header labels\Contao\DataContainer
Data Container object
return: array
Header labels
Example
// src/EventListener/DataContainer/CalendarHeaderCallback.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use Doctrine\DBAL\Connection;
/**
* Adds the total number of events to the header fields.
*/
#[AsCallback(table: 'tl_calendar_events', target: 'list.sorting.header')]
class CalendarHeaderCallback
{
/** @var Connection */
private $db;
public function __construct(Connection $db)
{
$this->db = $db;
}
public function __invoke(array $labels, DataContainer $dc): array
{
$count = $this->db->fetchOne("SELECT COUNT(*) FROM tl_calendar_events WHERE pid = ?", [$dc->id]);
$labels['Total events'] = $count;
return $labels;
}
}
list.sorting.panel_callback.subpanel
This callback allows you to inject HTML for custom panels. Replace subpanel
wit your custom panel’s name.
Parameters
\Contao\DataContainer
Data Container object
return: string
HTML for panel
list.label.group
Allows for individual group headers in the listing.
Parameters
string
Groupstring
Modestring
Fieldarray
Record data\Contao\DataContainer
Data Container object
return: string
The group to be grouped by
list.label.label
Allows for individual labels in the listing and is e.g. used in the user module
to add status icons.
Parameters
Tree view
array
Record datastring
Current label\Contao\DataContainer
Data Container objectstring
Always emptybool
Always falsebool
Whether the record is protected
return: string
The record label
List view
array
Record datastring
Current label\Contao\DataContainer
Data Container objectarray
Columns with existing labels
return: If the DCA uses showColumns
then the return value must be an array
of strings. Otherwise just the label as a string
.
Parent view
array
Record datastring
Current label\Contao\DataContainer
Data Container object
return: string
The record label
Example for tree view
This example adds an icon to the label of the example entity as tree view.
// src/EventListener/DataContainer/ExampleLabelCallbackListener.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use Contao\Image;
#[AsCallback(table: 'tl_example', target: 'list.label.label')]
class ExampleLabelCallbackListener
{
public function __invoke(array $row, string $label, DataContainer $dc, string $imageAttribute = '', bool $returnImage = false, ?bool $isProtected = null): string
{
$icon = Image::getHtml('bundles/app/images/example.svg');
return $icon.sprintf(' %s <span class="tl_gray" style="margin-left:3px;">[%s]</span>', $row['title'], $row['name']);
}
}
Example for list view
This example translates a dynamic status field for the label of the example entity as list view.
// src/EventListener/DataContainer/ExampleLabelCallbackListener.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCallback(table: 'tl_example', target: 'list.label.label')]
class ExampleLabelCallbackListener
{
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function __invoke(array $row, string $label, DataContainer $dc, array $labels): array
{
$fieldName = 'status';
$fields = $GLOBALS['TL_DCA'][$dc->table]['list']['label']['fields'];
$key = array_search($fieldName, $fields, true);
$labels[$key] = $this->translator->trans('tl_example.status_option.'.$labels[$key], [], 'contao_tl_example') ?? $this->translator->trans('tl_example.status_option_unknown', [], 'contao_tl_example');
return $labels;
}
}
Operations callbacks
All operations callbacks are singular callbacks - meaning there can only be
one callback, not multiple ones.
The following is a list of callbacks for operations. Replace <OPERATION>
in each case with the actual operation you want to use the callback for.
These callbacks allow for individual navigation icons and is e.g. used in the
site structure to disable buttons depending on the user’s permissions (requires
an additional command check via a fields.<field>.load
callback).
This callback allows you to generate a button for a specific global operation yourself, instead of letting Contao
generate it for you. The callback passes the originally generated button HTML as a string (if applicable), plus all the
metadata defined in the DCA that is included in the generated button. The callback is expected to return a string
containing HTML for the button (or an empty string, if you do not want to show a button).
Parameters
string
/null
Button hrefstring
Labelstring
Titlestring
Classstring
HTML attributesstring
Tablearray
IDs of all root records
return: string
HTML for the button
This callback allows you to generate a button for a specific operation yourself, instead of letting Contao generate it
for you. The callback passes the database record, the originally generated button HTML as a string (if applicable) and
all the metadata defined in the DCA that is included in the generated button. The callback is expected to return a string
containing HTML for the button (or an empty string, if you do not want to show a button).
Parameters
array
Record datastring
/null
Button hrefstring
Labelstring
Titlestring
/null
Iconstring
HTML attributesstring
Tablearray
IDs of all root recordsarray
/null
IDs of all child recordsbool
Whether this is a circular reference of the tree viewstring
/null
“Previous” labelstring
/null
“Next” label\Contao\DataContainer
Data Container object
return: string
HTML for the button
Example
This example hide a custom operation button if the user is not allowed to use it.
Attention: this won’t disable the operation itself, it only hides the button!
To disable the operation, you need to check for the permission additionally
before its execution, for example in the operation code or a config.onload
callback.
// src/EventListener/DataContainer/ExampleListOperationListener.php
namespace App\EventListener\DataContainer;
use Contao\Backend;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use Contao\Image;
use Contao\StringUtil;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
#[AsCallback(table: 'tl_example', target: 'list.operations.custom.button')]
class ExampleListOperationListener
{
public function __construct(
private readonly AuthorizationCheckerInterface $authorizationChecker,
) {
}
public function __invoke(
array $row,
?string $href,
string $label,
string $title,
?string $icon,
string $attributes,
string $table,
array $rootRecordIds,
?array $childRecordIds,
bool $circularReference,
?string $previous,
?string $next,
DataContainer $dc
): string
{
if (!$this->authorizationChecker->isGranted('contao_user.example', 'custom_operation')) {
return '';
}
return sprintf(
'<a href="%s" title="%s"%s>%s</a> ',
Backend::addToUrl($href . '&id=' . $row['id']),
StringUtil::specialchars($title),
$attributes,
Image::getHtml($icon, $label)
);
}
}
Field Callbacks
The following is a list of callbacks for DCA fields. Replace <FIELD>
with a
field name of your choice.
fields.<FIELD>.attributes
This feature is available in Contao 5.1 and later.
Allows you do dynamically adjust the attributes of a field in a DCA before a widget is generated.
Parameters
array
Current attributes\Contao\DataContainer
/null
Data Container object
return: array
The adjusted attributes array
Example
// src/EventListener/DataContainer/AttributesCallback.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
#[AsCallback('tl_content', 'fields.text.attributes')]
class AttributesCallback
{
public function __invoke(array $attributes, DataContainer|null $dc = null): array
{
if (!$dc || 'text' !== ($dc->getCurrentRecord()['type'] ?? null)) {
return $attributes;
}
$attributes['label'] = 'Custom text label';
return $attributes;
}
}
fields.<FIELD>.options
The fields.<FIELD>.options
callback is a singular callback - meaning there can
only be one callback, not multiple ones.
Allows you to define an individual function to load data into a drop-down menu
or checkbox list. Useful e.g. for conditional foreinKey-relations.
Parameters
\Contao\DataContainer
/null
Data Container object
return: array
Array of available options
The fields.<FIELD>.input_field
callback is a singular callback - meaning there
can
only be one callback, not multiple ones.
Allows for the creation of individual form fields and is e.g. used in the back
end module “personal data” to generate the “purge data” widget. Attention: the
field is not saved automatically!
Parameters
\Contao\DataContainer
Data Container objectstring
Extended label
return: string
HTML output for the field
fields.<FIELD>.load
Executed when a form field is initialized and can e.g. be used to load a default
value.
Parameters
Back end
mixed
Currently stored value\Contao\DataContainer
Data Container object
return: mixed
New value to be loaded
Front end module “Personal data”
mixed
Currently stored value\Contao\FrontendUser
The front end user instance\Contao\ModulePersonalData
The front end module instance
return: mixed
New value to be loaded
fields.<FIELD>.save
Executed when a field is submitted and can e.g. be used to add an individual
validation routine. If the new value does not validate, you can throw an
\Exception
with an appropriate error message. The record will not be saved
then and the error message will be shown in the form.
Parameters
Back end
mixed
Value to be saved\Contao\DataContainer
Data Container object
return: mixed
New value to be saved
Front end module “Personal data”
mixed
Value to be saved\Contao\FrontendUser
The front end user instance\Contao\ModulePersonalData
The front end module instance
return: mixed
New value to be saved
Front end module “Registration”
return: mixed
New value to be saved
Example
// src/EventListener/DataContainer/ContentTextSaveCallback.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
#[AsCallback(table: 'tl_content', target: 'fields.text.save')]
class ContentTextSaveCallback
{
public function __invoke($value, DataContainer $dc)
{
// Show an error if tl_content.text contains "foobar"
if (false !== stripos($value, 'foobar')) {
throw new \Exception('String "foobar" is not allowed.');
}
// Or process the value before saving
$value = strtoupper($value);
// Return the processed value
return $value;
}
}
fields.<FIELD>.wizard
Allows you to add additional HTML after the field input, typically used to show
a button that starts a “wizard”.
Parameters
\Contao\DataContainer
Data Container object
return: string
HTML for the button
fields.<FIELD>.xlabel
Allows you to add additional HTML after the field label, typically used to show
a button for an import “wizard”.
Parameters
\Contao\DataContainer
Data Container object
return: string
HTML for the button
fields.<FIELD>.eval.url
Allows you to add an url to the serp preview field.
Parameters
\Contao\Model
Model object (class from the table)
return: string
URL for the serp preview
Example
// src/EventListener/DataContainer/ExampleSerpPreviewUrlCallbackListener.php
namespace App\EventListener\DataContainer;
use App\Model\ExampleCategoryModel;
use App\Model\ExampleModel;
use Contao\Config;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\PageModel;
#[AsCallback(table: 'tl_example', target: 'fields.serpPreview.eval.url')]
class ExampleSerpPreviewUrlCallbackListener
{
public function __invoke(ExampleModel $model): string
{
/** @var ExampleCategoryModel $category */
$category = $model->getRelated('pid');
if (null === $category) {
throw new \Exception('Invalid category');
}
/** @var PageModel $page */
$page = $category->getRelated('jumpTo');
if (null === $page) {
throw new \Exception('Invalid jumpTo page');
}
$suffix = $page->getAbsoluteUrl(Config::get('useAutoItem') ? '/%s' : '/items/%s');
return sprintf(preg_replace('/%(?!s)/', '%%', $suffix), $model->alias ?: $model->id);
}
}
fields.<FIELD>.eval.title_tag
Allows you to modify the title tag of the serp preview field.
Parameters
\Contao\Model
Model object (class from the table)
return: string
title tag for the serp preview
Example
// src/EventListener/DataContainer/ExampleSerpPreviewTitleTagCallbackListener.php
namespace App\EventListener\DataContainer;
use App\Model\ExampleCategoryModel;
use App\Model\ExampleModel;
use Contao\Controller;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\LayoutModel;
use Contao\PageModel;
#[AsCallback(table: 'tl_example', target: 'fields.serpPreview.eval.title_tag')]
class ExampleSerpPreviewTitleTagCallbackListener
{
public function __invoke(ExampleModel $model): string
{
/** @var ExampleCategoryModel $category */
$category = $model->getRelated('pid');
if (null === $category) {
return '';
}
/** @var PageModel $page */
$page = $category->getRelated('jumpTo');
if (null === $page) {
return '';
}
$page->loadDetails();
/** @var LayoutModel $layout */
$layout = $page->getRelated('layout');
if (null === $layout) {
return '';
}
global $objPage;
$objPage = $page;
return Controller::replaceInsertTags(str_replace('{{page::pageTitle}}', '%s', $layout->titleTag ?: '{{page::pageTitle}} - {{page::rootPageTitle}}'));
}
}
Edit Callbacks
The following is a list of callbacks relating to edit actions of a Data Container.
Allows you to modify the action buttons at the bottom of record editing form. This
can be used to add additional buttons or remove any of the existing buttons.
Parameters
array
Array of strings\Contao\DataContainer
Data Container object
return: array
Array of strings containing the buttons’ markup
Example
This example removes the “Save and close” button from the editing form of a record
of the tl_example
Data Container.
// src/EventListener/DataContainer/EditButtonsCallbackListener.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
#[AsCallback(table: 'tl_example', target: 'edit.buttons')]
class EditButtonsCallbackListener
{
public function __invoke(array $buttons, DataContainer $dc): array
{
// Remove the "Save and close" button
unset($buttons['saveNclose']);
return $buttons;
}
}
Select Callbacks
The following is a list of callbacks relating to select actions of a Data Container.
Allows you to modify the action buttons at the bottom after selecting rows. This
can be used to add additional buttons or update and remove any of the existing buttons.
Parameters
array
Array of strings\Contao\DataContainer
Data Container object
return: array
Array of strings containing the buttons’ markup
Example
This example removes the “delete” button from the editing form of a record
of the tl_example
Data Container.
// src/EventListener/DataContainer/SelectButtonsCallbackListener.php
namespace App\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
#[AsCallback(table: 'tl_example', target: 'select.buttons')]
class SelectButtonsCallbackListener
{
public function __invoke(array $buttons, DataContainer $dc): array
{
// Remove the delete button
unset($buttons['delete']);
return $buttons;
}
}