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.

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 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:

img_register-services_02.png

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},