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. However, the detailed example at the
end of this page uses features (e.g. the ContentUrlGenerator as well as readonly
classes) and therefore requires at least Contao 5.3 and PHP 8.2.
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 example from the CK23 talk by Ingolf Steinhardt
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.
Examples of Services and Their Registration
Note
The examples require at least Contao 5.3 and PHP 8.2.
The following two files list typical services and show how they can be registered:
1services:
2 # Example of a listener..
3 AppBundle\EventListener\MetaModelsServiceExamplesListener:
4 public: true
5 arguments:
6 $factory: '@metamodels.factory'
7 $filterFactory: '@metamodels.filter_setting_factory'
8 $renderFactory: '@metamodels.render_setting_factory'
9 $connection: '@database_connection'
10 $logger: '@monolog.logger.contao'
11 $mailer: '@mailer'
12 $notificationCenter: '@Terminal42\NotificationCenterBundle\NotificationCenter'
13 $requestStack: '@request_stack'
14 $security: '@security.helper'
15 $tokenStorage: '@security.token_storage'
16 $framework: '@contao.framework'
17 $scopeMatcher: '@contao.routing.scope_matcher'
18 $scopeDeterminator: '@cca.dc-general.scope-matcher'
19 $urlGenerator: '@contao.routing.content_url_generator'
20 $httpClient: '@http_client'
21 $tokenParser: '@contao.string.simple_token_parser'
22 $inserttagParser: '@contao.insert_tag.parser'
23 $rootPath: '%kernel.project_dir%'
24 tags:
25 - { name: kernel.event_listener, event: dc-general.model.pre-persist, method: onMetaModelsServiceExamples }
1<?php
2
3namespace AppBundle\EventListener;
4
5use ContaoCommunityAlliance\DcGeneral\Contao\RequestScopeDeterminator;
6use ContaoCommunityAlliance\DcGeneral\Event\PrePersistModelEvent;
7use Contao\Controller;
8use Contao\CoreBundle\Framework\ContaoFramework;
9use Contao\CoreBundle\InsertTag\InsertTagParser;
10use Contao\CoreBundle\Monolog\ContaoContext;
11use Contao\CoreBundle\Routing\ContentUrlGenerator;
12use Contao\CoreBundle\Routing\ScopeMatcher;
13use Contao\CoreBundle\String\SimpleTokenParser;
14use Contao\FrontendUser;
15use Contao\MemberModel;
16use Contao\PageModel;
17use Doctrine\DBAL\Connection;
18use MetaModels\Filter\Setting\FilterSettingFactory;
19use MetaModels\IFactory;
20use MetaModels\IMetaModel;
21use MetaModels\Render\Setting\RenderSettingFactory;
22use Psr\Log\LoggerInterface;
23use Symfony\Component\HttpFoundation\RequestStack;
24use Symfony\Component\Mailer\MailerInterface;
25use Symfony\Component\Mime\Email;
26use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
27use Symfony\Bundle\SecurityBundle\Security;
28use Symfony\Contracts\HttpClient\HttpClientInterface;
29use Terminal42\NotificationCenterBundle\NotificationCenter;
30
31final readonly class MetaModelsServiceExamplesListener
32{
33 public function __construct(
34 private IFactory $factory,
35 private FilterSettingFactory $filterFactory,
36 private RenderSettingFactory $renderFactory,
37 private Connection $connection,
38 private LoggerInterface $logger,
39 private MailerInterface $mailer,
40 private NotificationCenter $notificationCenter,
41 private RequestStack $requestStack,
42 private Security $security,
43 private TokenStorageInterface $tokenStorage,
44 private ContaoFramework $framework,
45 private ScopeMatcher $scopeMatcher,
46 private RequestScopeDeterminator $scopeDeterminator,
47 private ContentUrlGenerator $urlGenerator,
48 private HttpClientInterface $httpClient,
49 private SimpleTokenParser $tokenParser,
50 private InsertTagParser $inserttagParser,
51 private string $rootPath
52 ) {
53 }
54
55 public function onMetaModelsServiceExamples(PrePersistModelEvent $event): void
56 {
57 // MetaModels.
58 $modelName = 'mm_my_model';
59 $filterId = 11;
60 $renderId = 22;
61 $model = $this->factory->getMetaModel($modelName);
62 assert($model instanceof IMetaModel);
63 $filter = $model->getEmptyFilter();
64 $filterCollection = $this->filterFactory->createCollection($filterId);
65 $filterCollection->addRules($filter, []);
66 $items = $model->findByFilter($filter);
67
68 if (!$items->getCount()) {
69 return;
70 }
71
72 $currentItem = $items
73 ->getItem()
74 ->parseValue('html5', $this->renderFactory->createCollection($model, $renderId));
75
76 // Database.
77 $isPublished = 1;
78 $modelData = $this->connection->createQueryBuilder()
79 ->select('t.*')
80 ->from('mm_my_model', 't')
81 ->where('published=:published')
82 ->setParameter('published', $isPublished)
83 ->executeQuery()
84 ->fetchAllAssociative();
85
86 // Logger.
87 $message = 'This is a message.';
88 $e = new \Exception('This is an exception.');
89 $this->logger->error($message, [
90 'contao' => new ContaoContext(__METHOD__, ContaoContext::ERROR),
91 'exception' => $e,
92 ]);
93
94 // Mailer.
95 // Symfony.
96 $message = (new Email())
97 ->from('webmaster@domain.de')
98 ->to('admin@domain.de')
99 ->replyTo('webmaster@domain.de')
100 ->subject('My subject')
101 ->text('My body');
102 $this->mailer->send($message);
103
104 // NotificationCenter.
105 $messageId = 42;
106 $tokens = [
107 'recipient_email' => 'admin@domain.de',
108 'form_name' => 'Blaubär',
109 'form_message' => 'My body',
110 'form_email' => 'blaubaer@home.de',
111 ];
112 $this->notificationCenter->sendNotification($messageId, $tokens);
113
114 // RequestStack.
115 $token = $this->requestStack->getCurrentRequest()->query->get('token');
116
117 // Session.
118 $request = $this->requestStack->getCurrentRequest();
119 if (!$request) {
120 throw new \Exception('Session or request not found!');
121 }
122 $sessionData = $request->getSession()->get('MM-DATA', []);
123 $sessionData['moin'] = 'my data';
124 $request->getSession()->set('MM-DATA', $sessionData); // Set session data.
125 $sessionData = $request->getSession()->get('MM-DATA', []); // Get session data.
126
127 // Security - more explanations in the following text section.
128 // Recommended: security.helper - can check permissions AND fetch the user.
129 if (!$this->security->isGranted('ROLE_MEMBER')) {
130 return;
131 }
132 if (!($user = $this->security->getUser()) instanceof FrontendUser) {
133 return;
134 }
135 // Reload the Contao MemberModel (with all DB fields) by username.
136 $member = MemberModel::findByUsername($user->getUserIdentifier());
137
138 // Alternative 1: Contao framework (legacy). getInstance() returns the current
139 // frontend user as a Contao object including the DB fields - createInstance()
140 // would be a new, empty instance and therefore wrong.
141 $member = $this->framework->getAdapter(FrontendUser::class)->getInstance();
142
143 // Alternative 2: security.token_storage (low-level, no permission check).
144 $token = $this->tokenStorage->getToken();
145 if (null === $token || !($user = $token->getUser()) instanceof FrontendUser) {
146 return;
147 }
148 $userGroups = $user->groups;
149
150 // Scope.
151 // Contao.
152 $currentRequest = $this->requestStack->getCurrentRequest();
153 $isFrontend = $this->scopeMatcher->isFrontend($currentRequest);
154 $isBackend = $this->scopeMatcher->isBackend($currentRequest);
155
156 // DC_General.
157 $isFrontend = $this->scopeDeterminator->isFrontend();
158 $isBackend = $this->scopeDeterminator->isBackend();
159
160 // URL.
161 if (null === ($page = PageModel::findByPk(42))) {
162 return;
163 }
164 $url = $this->urlGenerator->generate($page);
165 Controller::redirect($url);
166
167 // HTTP Client.
168 $httpClient = $this->httpClient;
169 $url = 'https://api.domain.de/v1/my-endpoint';
170 $options = ['query' => ['foo' => 'bar']];
171 $response = $httpClient->request('GET', $url, $options);
172 if (200 !== $response->getStatusCode()) {
173 return;
174 }
175 $data = $response->toArray();
176
177 // Parser.
178 // Token.
179 $subject = 'Hello ##firstname## ##lastname##!';
180 $tokens = ['firstname' => 'John', 'lastname' => 'Doe'];
181 $message = $this->tokenParser->parse($subject, $tokens);
182
183 // Inserttag.
184 $content = '<a href="{{link_url::4711}}">read more</a>';
185 $content = $this->inserttagParser->parse($content);
186
187 // Root path.
188 $filePath = 'files/my_folder/moinmoin.pdf';
189 if (\file_exists($this->rootPath . '/' . $filePath)) {
190 $file = $this->rootPath . '/' . $filePath;
191 }
192 }
193}
Determining the Current User (Security)
To determine the currently logged-in frontend user, the example shows three approaches
that operate at different levels. The first approach, via security.helper, is
recommended.
Recommended – security.helper
The security.helper (Symfony\Bundle\SecurityBundle\Security) bundles the
AuthorizationChecker and the TokenStorage. It is the only one of the three
approaches that can both check permissions (isGranted()) and fetch the
user (getUser()). getUser() returns the Symfony user object; the Contao
MemberModel with all database fields can then be loaded afterwards via
findByUsername().
Advantage: modern standard, covers both permission checking and user retrieval.
Disadvantage: an additional query is required for the raw DB fields.
Alternative 1 – contao.framework (Legacy)
The classic Contao approach via the framework adapter. Important: the getInstance()
singleton is intended for the current user — createInstance() would create a new,
empty instance and would therefore be wrong.
Advantage: directly returns the Contao user object including DB fields (e.g.
$member->email) without an additional query.Disadvantage: Contao-specific, no permission checking, outdated pattern, only useful within the frontend scope.
Alternative 2 – security.token_storage (Low-Level)
The pure Symfony variant only returns the token or the user — without an
AuthorizationChecker. It is exactly the foundation on which security.helper is
internally built.
Advantage: minimal, works everywhere.
Disadvantage: cannot check permissions; null handling (not logged in = no token) must be handled manually.
In short: use security.helper as the default. Use token_storage only when you
deliberately need just the token, and framework/getInstance() only for legacy
code or when you need the Contao model with its fields directly.