Registrierung von Services
Bemerkung
Die im Folgenden vorgestellten Möglichkeiten sind lediglich Vorschläge bzw. Beispiele - für die eigene Arbeit sollte man sich seine optimale Konfiguration entwickeln - mehr zu dem Thema ist in der Symfony-Dokumentation zu finden.
MetaModels bringt viele Funktionen mit, die man lediglich im Backend aktivieren oder konfigurieren muss. Dennoch können damit nicht alle erdenklichen Einstellungen und Funktionen abgedeckt werden. Bei individuellen Projektaufgaben können die implementierten Möglichkeiten nicht ausreichen und müssen um eigene Anpassungen ergänzt werden.
Hier stehen verschiedene Methoden von MM oder auch von DC_General (DCG) zur Verfügung, um mit wenigen Zeilen diese Aufgaben umzusetzen.
Insbesondere die zur Verfügung gestellten Events bieten dabei eine einfache Möglichkeit, eine eigene Logik zu implementieren bzw. in die vorhandene einzugreifen. Ein Einstieg in die Arbeit mit der MetaModels Referenz und API bietet z. B. der Vortrag von Ingolf Steinhardt zur CK23
Folgend werden verschiedene Varianten zur Implementierung anhand des PrePersistModelEvents aufgeführt. Das Event wird von der Eingabemaske „kurz vor dem Speichern in die DB“ aufgerufen, sofern sich ein Feldwert geändert hat. Mit dem Event können z. B. eingegebene Daten manipuliert oder neue dynamisch generiert werden.
Die EventListener oder auch andere Services werden analog den Contao-Hooks registriert.
Bemerkung
Vorausgesetzt wird mind. Contao 4.13 und PHP 8. Das ausführliche Beispiel am Ende der Seite nutzt
allerdings Funktionen (z. B. den ContentUrlGenerator sowie readonly Klassen) und setzt daher
mind. Contao 5.3 und PHP 8.2 voraus.
1. Registrierung per Attribut
Die Registrierung per Attribut bietet die einfachste Variante der Implementierung - es muss lediglich folgende Datei angelegt und der Cache geleert werden.
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}
Nach dem leeren des Caches kann die Registrierung wie folgt überprüft werden
php vendor/bin/contao-console debug:event-dispatcher dc-general.model.pre-persist
Der Key dc-general.model.pre-persist steht in der jeweiligen Klasse und kann ebenfalls als Parameter in dem Attribut
verwendet werden. War die Registrierung erfolgreich, sollte der markierte Eintrag zu finden sein.

Sollte das noch nicht der Fall sein, kann die Ausführung eines composer install Abhilfe schaffen.
Wenn die ausführende Methode den Namen __invoke hat, kann der Attributsschlüssel wie in dem Beispiel an den
Klassennamen geschrieben werden - wenn man einen individuellen Methodennamen einsetzen möchte z. B. wenn mehrere
Methoden in einer Klasse zu verschiedenen Events vorhanden sind, muss der Attributsschlüssel an den jeweiligen
Methodennamen.
Diese Variante funktioniert in dieser einfachen Form nur, wenn nicht weitere Events o. ä. über die services.yml
registriert werden. Ist dies der Fall, kann man entweder ganz auf die Registrierung über die services.yml umsteigen -
siehe Punkt 2 - oder man fügt folgende Zeilen in die services.yml, um ein automatisches Laden zu erwirken:
1# config/services.yml
2services:
3 _defaults:
4 autowire: true
5 autoconfigure: true
6 public: false
7
8 App\:
9 resource: '../src/*'
2. Registrierung ohne Attribut per services.yml
Als Alternative zur Registrierung per Attribut kann man den Aufruf über die services.yml einbinden - insbesondere, wenn man verschiedene Einstellungen hat und sich auf die automatische Registrierung nicht verlassen möchte.
Die Klasse sieht dann wie folgt aus:
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}
Zudem muss in die services.yml folgender Eintrag:
1# config/services.yml
2services:
3 App\EventListener\PrePersistModelEventListener:
4 tags:
5 - { name: kernel.event_listener, event: dc-general.model.pre-persist }
Sofern die Methode nicht die Bezeichnung __invoke hat, muss bei den Tags der services.yml der Methodenname
ergänzt werden - zudem ist die Angabe einer Priorität möglich. Mehr dazu bei
Symfony
3. Registrierung per Attribut und Einbindung weiterer Services
Benötigt man in seiner Klasse den Zugriff auf weitere Services, kann man die über den constructor
automatisch einbinden.
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. Registrierung ohne Attribut per services.yml und Einbindung weiterer Services
Benötigt man in seiner Klasse den Zugriff auf weitere Services, kann man die über den constructor einbinden, indem
man den Service in der services.yml als Argument übergibt.
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. Alle Dateien in src/ und Namespace App
Möchte man zur einfacheren Datenpflege alle Dateien - also auch z. B. die service.yml - kompakt im Ordner src/
haben aber dennoch mit dem Namespace App arbeiten, so kann man sich das Beispiel vom Vortrag von Ingolf Steinhardt
zur CK23 ansehen bzw. den
Ordner src/ zum Testen downloaden und die composer.json entsprechend anpassen.
Zu beachten ist der Eintrag foo - der ist notwendig um einige „Contao-Magic“ für den Namespace zu umgehen…
6. Alle Dateien in src/ und eigene Bundles
Möchte man mit einem eigenen Namespace arbeiten und weniger Contao- bzw. Symfony-Magic, so müssen einige Dateien mehr
in src/ angelegt werden. Das kann z. B. dann sinnvoll sein, wenn man mit mehreren separaten Bundles und ihren
Namespaces arbeiten möchte. In dem Fall, würde man weitere Unterordner z. B. src/ProjectOneBundle anlegen.
Ist dies nicht der Fall, können alle Dateien direkt in src/ mit dem Namespace z. B. AppBundle.
Beispiele von Services und deren Einbindung
Bemerkung
Vorausgesetzt für die Beispiele wird mind. Contao 5.3 und PHP 8.2.
Mit den zwei Dateien sind typische Services aufgeführt und wie diese eingebunden werden können:
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 - mehr Erklärungen in dem anschließenden Textabschnitt.
128 // Empfohlen: security.helper - kann Rechte prüfen UND den User holen.
129 if (!$this->security->isGranted('ROLE_MEMBER')) {
130 return;
131 }
132 if (!($user = $this->security->getUser()) instanceof FrontendUser) {
133 return;
134 }
135 // Contao-MemberModel (mit allen DB-Feldern) per Username nachladen.
136 $member = MemberModel::findByUsername($user->getUserIdentifier());
137
138 // Alternative 1: Contao-Framework (Legacy). getInstance() liefert den aktuellen
139 // Frontend-User als Contao-Objekt inkl. DB-Feldern - createInstance() wäre eine
140 // neue, leere Instanz und damit falsch.
141 $member = $this->framework->getAdapter(FrontendUser::class)->getInstance();
142
143 // Alternative 2: security.token_storage (Low-Level, kein Rechte-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}
Den aktuellen Benutzer ermitteln (Security)
Um den aktuell eingeloggten Frontend-Benutzer zu ermitteln, gibt es im Beispiel drei Wege, die auf
unterschiedlichen Ebenen arbeiten. Empfohlen ist der erste Weg über den security.helper.
Empfohlen – security.helper
Der security.helper (Symfony\Bundle\SecurityBundle\Security) bündelt den AuthorizationChecker
und die TokenStorage. Er ist der einzige der drei Wege, der sowohl Rechte prüfen (isGranted())
als auch den User holen (getUser()) kann. getUser() liefert das Symfony-User-Objekt; das
Contao-MemberModel mit allen Datenbankfeldern lädt man anschließend per findByUsername() nach.
Vorteil: moderner Standard, deckt Rechteprüfung und User-Ermittlung ab.
Nachteil: für die reinen DB-Felder ist ein zusätzlicher Query nötig.
Alternative 1 – contao.framework (Legacy)
Der klassische Contao-Weg über den Framework-Adapter. Wichtig: Für den aktuellen Benutzer ist das
Singleton getInstance() gedacht - createInstance() würde eine neue, leere Instanz erzeugen und wäre
damit falsch.
Vorteil: liefert direkt das Contao-User-Objekt inkl. DB-Feldern (z. B.
$member->email) ohne zusätzlichen Query.Nachteil: Contao-spezifisch, keine Rechteprüfung, veraltetes Muster, nur im Frontend-Scope sinnvoll.
Alternative 2 – security.token_storage (Low-Level)
Die reine Symfony-Variante liefert nur Token bzw. User - ohne AuthorizationChecker. Sie ist genau die
Basis, auf der der security.helper intern aufsetzt.
Vorteil: minimal, funktioniert überall.
Nachteil: kann keine Rechte prüfen; das null-Handling (nicht eingeloggt = kein Token) muss selbst behandelt werden.
Kurz: security.helper als Standard verwenden. token_storage nur, wenn man bewusst nur das Token
braucht, und framework/getInstance() nur für Legacy oder wenn man direkt das Contao-Model mit seinen
Feldern benötigt.