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.

img_register-services_01.png

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.

Recommendedsecurity.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 1contao.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 2security.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.