Registering Services
Note
The options presented below are merely suggestions or examples — for your own work, you should develop the optimal configuration for your needs. More on the topic can be found in the Symfony documentation.
MetaModels comes with many functions that only need to be activated or configured in the backend. However, not every conceivable setting and function can be covered. For individual project tasks, the built-in options may not be sufficient and must be supplemented with custom adjustments.
Various MM and DC_General (DCG) methods are available here to accomplish these tasks in just a few lines.
In particular, the provided events offer a simple way to implement custom logic or hook into the existing logic. An introduction to working with the MetaModels Reference and API is provided e.g. by the CK23 talk by Ingolf Steinhardt.
The following presents various implementation approaches using the PrePersistModelEvent as an example. The event is called by the input mask “just before saving to the DB”, provided that a field value has changed. With this event, entered data can e.g. be manipulated or new data dynamically generated.
Event listeners and other services are registered analogously to Contao hooks.
Note
Requires at least Contao 4.13 and PHP 8
1. Registration via Attribute
Registration via attribute is the simplest implementation option — only the following file needs to be created and the cache cleared.
1<?php
2// src/EventListener/PrePersistModelEventListener.php
3namespace App\EventListener;
4
5use ContaoCommunityAlliance\DcGeneral\Event\PrePersistModelEvent;
6use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
7
8#[AsEventListener(PrePersistModelEvent::NAME)]
9class PrePersistModelEventListener
10{
11 public function __invoke(PrePersistModelEvent $event)
12 {
13 if ('mm_employees' !== $event->getEnvironment()->getDataDefinition()?->getName()) {
14 return;
15 }
16
17 $model = $event->getModel();
18 }
19}
After clearing the cache, the registration can be verified as follows:
php vendor/bin/contao-console debug:event-dispatcher dc-general.model.pre-persist
The key dc-general.model.pre-persist is defined in the respective class and
can also be used as a parameter in the attribute. If registration was successful,
the marked entry should be found.

If this is not yet the case, running composer install may resolve the issue.
If the executing method is named __invoke, the attribute key can be written
at the class name as in the example — if you want to use a custom method name,
e.g. when multiple methods for different events exist in one class, the attribute
key must be placed on the respective method name.
This approach works in this simple form only if no further events or similar are
registered via services.yml. If this is the case, you can either switch
entirely to registration via services.yml — see item 2 — or add the
following lines to services.yml to enable automatic loading:
1# config/services.yml
2services:
3 _defaults:
4 autowire: true
5 autoconfigure: true
6 public: false
7
8 App\:
9 resource: '../src/*'
2. Registration Without Attribute via services.yml
As an alternative to registration via attribute, the call can be included via
services.yml — especially if you have various settings and do not want to
rely on automatic registration.
The class then looks as follows:
1<?php
2// src/EventListener/PrePersistModelEventListener.php
3namespace App\EventListener;
4
5use ContaoCommunityAlliance\DcGeneral\Event\PrePersistModelEvent;
6
7class PrePersistModelEventListener
8{
9 public function __invoke(PrePersistModelEvent $event)
10 {
11 if ('mm_employees' !== $event->getEnvironment()->getDataDefinition()?->getName()) {
12 return;
13 }
14
15 $model = $event->getModel();
16 }
17}
The following entry must also be added to services.yml:
1# config/services.yml
2services:
3 App\EventListener\PrePersistModelEventListener:
4 tags:
5 - { name: kernel.event_listener, event: dc-general.model.pre-persist }
If the method is not named __invoke, the method name must be added to the
tags in services.yml — a priority can also be specified. More at
Symfony.
3. Registration via Attribute with Additional Services
If access to further services is needed in the class, they can be automatically
injected via the constructor.
1<?php
2// src/EventListener/PrePersistModelEventListener.php
3namespace App\EventListener;
4
5use ContaoCommunityAlliance\DcGeneral\Event\PrePersistModelEvent;
6use MetaModels\IFactory;
7use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
8
9#[AsEventListener(PrePersistModelEvent::NAME)]
10class PrePersistModelEventListener
11{
12 public function __construct(private readonly IFactory $factory)
13 {
14 }
15
16 public function __invoke(PrePersistModelEvent $event)
17 {
18 if ('mm_employees' !== $event->getEnvironment()->getDataDefinition()?->getName()) {
19 return;
20 }
21
22 $model = $event->getModel();
23
24 $anotherMetaModel = $this->factory->getMetaModel('mm_another_model');
25 }
26}
4. Registration Without Attribute via services.yml with Additional Services
If access to further services is needed in the class, they can be injected via
the constructor by passing the service as an argument in services.yml.
1<?php
2// src/EventListener/PrePersistModelEventListener.php
3namespace App\EventListener;
4
5use ContaoCommunityAlliance\DcGeneral\Event\PrePersistModelEvent;
6use MetaModels\IFactory;
7
8class PrePersistModelEventListener
9{
10 public function __construct(private readonly IFactory $factory)
11 {
12 }
13
14 public function __invoke(PrePersistModelEvent $event)
15 {
16 if ('mm_employees' !== $event->getEnvironment()->getDataDefinition()?->getName()) {
17 return;
18 }
19
20 $model = $event->getModel();
21
22 $anotherMetaModel = $this->factory->getMetaModel('mm_another_model');
23 }
24}
1# config/services.yml
2services:
3 App\EventListener\PrePersistModelEventListener:
4 arguments:
5 - '@metamodels.factory'
6 tags:
7 - { name: kernel.event_listener, event: dc-general.model.pre-persist }
5. All Files in src/ with Namespace App
If you want to keep all files — including e.g. service.yml — compactly in
the src/ folder while still working with the App namespace, you can look
at the CK23 talk example on
Github
or download the src/ folder for testing and adjust composer.json
accordingly.
Note the entry foo — it is required to work around some “Contao magic” for the namespace…
6. All Files in src/ with Custom Bundles
If you want to work with your own namespace and less Contao/Symfony magic, more
files need to be created in src/. This can be useful e.g. when working with
multiple separate bundles and their namespaces. In that case, additional
subfolders such as src/ProjectOneBundle would be created.
If this is not the case, all files can be placed directly in src/ with a
namespace such as AppBundle.
The following shows an example structure:

For the files and namespace to be found correctly, composer.json must be
extended as follows:
1"autoload": {
2 "psr-4": {
3 "AppBundle\\": "src/"
4 }
5},
The following two files are essential for the basic setup:
1<?php
2// src/AppBundle.php
3namespace AppBundle;
4
5use Symfony\Component\HttpKernel\Bundle\Bundle;
6
7/**
8 * This is the local customization bundle.
9 */
10class AppBundle extends Bundle
11{
12}
1<?php
2// src/DependencyInjection/AppExtension.php
3namespace AppBundle\DependencyInjection;
4
5use Symfony\Component\Config\FileLocator;
6use Symfony\Component\DependencyInjection\ContainerBuilder;
7use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
8use Symfony\Component\HttpKernel\DependencyInjection\Extension;
9
10class AppExtension extends Extension
11{
12
13 /**
14 * Loads a specific configuration.
15 *
16 * @throws \InvalidArgumentException When provided tag is not defined in this extension
17 */
18 public function load(array $configs, ContainerBuilder $container)
19 {
20 $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
21 $loader->load('services.yml');
22 }
23}
The following file ContaoManagerPlugin.php is optional and controls the
order in which the custom bundle is loaded relative to other bundles. Here you
can e.g. specify that your own bundle is loaded after Contao — other bundles
such as the NotificationCenter can also be specified. For the file to be
recognised, this must be stated in composer.json — see below.
1<?php
2// src/ContaoManager/ContaoManagerPlugin.php
3
4use Contao\CoreBundle\ContaoCoreBundle;
5use Contao\ManagerBundle\ContaoManagerBundle;
6use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
7use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
8use Contao\ManagerPlugin\Bundle\Config\ConfigInterface;
9use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
10
11class ContaoManagerPlugin implements BundlePluginInterface
12{
13 /**
14 * Gets a list of autoload configurations for this bundle.
15 *
16 * @param ParserInterface $parser
17 *
18 * @return array<ConfigInterface>
19 */
20 public function getBundles(ParserInterface $parser): array
21 {
22 return [
23 BundleConfig::create(AppBundle::class)
24 ->setLoadAfter(
25 [
26 ContaoCoreBundle::class,
27 ContaoManagerBundle::class
28 ]
29 )
30 ];
31 }
32}
1"autoload": {
2 "psr-4": {
3 "AppBundle\\": "src/"
4 },
5 "classmap": [
6 "src/ContaoManager/ContaoManagerPlugin.php"
7 ]
8},