IDCI logo
French flag English flag

The Symfony CompilerPass

When you are creating a Bundle, you often need to defined a configuration for a service inside a bundle, but this configuration depends on the app. This is the CompilerPass function.

The cycle of compilation of a Symfony app

First, it's relevant to understand how a Symfony app is built. This process is mostly triggered when the app cache is empty.

On a request input, the Kernel builds the services from bundles and configurations, then caches them. The Kernel is the entrypoint.

During the container building phase, the Kernel inject the ContainerBuilder in every bundle through their build() method, and these bundles add their CompilerPass list to the ContainerBuilder.

During this phase, nothing is instanciated, Symonfy is just collecting the services definitions list.

Then, comes the compilation phase. The definitions consistency is verified, it is at this moment that the CompilerPass intervene, and that errors might occur if there is circular dependencies. The CompilerPass are modifying the definitions previously gathered.

Finally, these services are compiled in a ServiceContainer and cached for optimal performances, usually in "var/cache/".

Schema of Symfony compilation cycle

The ServiceContainer (or dependencies injection container)

We mentionned the ServiceContainer in the previous part. Its role is to centralize the creation and the distribution of all app PHP objects.

Thus, it knows the services list, and manages instanciation when they are called by the app.

The CompilerPass

The CompilerPass allows to intervene on the services configuration dynamically, just before ServiceContainer compilation.

This configuration usually is defined through a yaml file.

Where to put it ?

Inside your Symfony Bundle tree, inside src > DependencyInjection > Compiler :

IDCI/
├─ HelloWorldBundle/
│ ├─ src/
│ │ ├─ DependencyInjection
│ │ │ ├─ Compiler
│ │ │ │ ├─ DebugDataCompilerPass.php
│ │ ├─ HelloWorldBundle.php
│ ├─ composer.json

You can create as much CompilerPass as you want.

How to use it ?

In the following examples, we will allow to add as much lines as we want inside the HelloWorldBundle tab of the debug toolbar through a static configuration, and we will create a DummyBundle to add or modify the rows with a CompilerPass.

First, we will update HelloWorldBundle to allow extra data display.

<?php

namespace IDCI\Bundle\HelloWorldBundle\DataCollector;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;

class HelloWorldDataCollector extends DataCollector
{
    public function __construct(
        ...
        private array $extraData,
    ) {
    }

    public function collect(Request $request, Response $response, \Throwable $exception = null): void
    {
        ...
        $this->data['hello_world.extra_data'] = $this->extraData;
    }

    ...

    public function getExtraData(): array
    {
        return $this->data['hello_world.extra_data'];
    }

    public function addExtraData(array $data): void {
        $key = array_search($data['label'], array_column($this->extraData, 'label'));

        if (false !== $key) {
            $this->extraData[$key] = $data;

            return;
        }

        $this->extraData[] = $data;
    }
}

To understand how to defined the parameters and inject them in the ServiceContainer, visit this previous article.

Now, add the informations into the app configuration file :

hello_world:
  app:
    name: 'test'
    version: 'v0'
  additional_datas:
    - { label: 'Message', value: 'Hello World !' }
    - { label: 'Active', value: true }
    - { label: 'Time', value: 10 }

Here's the result :

Symfony debug toolbar with extra data via yaml config

Now, let's make the DummyBundle, a CompilerPass must implements the CompilerPassInterface. This interface only define a single method: process(ContainerBuilder $container).

<?php

namespace IDCI\Bundle\DummyBundle\DependencyInjection\Compiler;

use IDCI\Bundle\HelloWorldBundle\DataCollector\HelloWorldDataCollector;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AddExtraDebugDataCompilerPass implements CompilerPassInterface
{
    public const EXTRA_DATA = [
        [
            'label' => 'Dummy',
            'value' => 'v0',
        ],
        [
            'label' => 'Message',
            'value' => 'Hello from Dummy !'
        ]
    ];

    public function process(ContainerBuilder $container): void
    {
        if (!$container->has(HelloWorldDataCollector::class)) {
            return;
        }

        $helloWorldDataCollectorDefinition = $container->findDefinition(HelloWorldDataCollector::class);

        foreach (self::EXTRA_DATA as $data) {
            $helloWorldDataCollectorDefinition->addMethodCall('addExtraData', [$data]);
        }
    }
}

For our CompilerPass to be taken into account, it should be called in the DummyBundle class, in the bundle's src folder.

<?php

namespace IDCI\Bundle\DummyBundle;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
use IDCI\Bundle\DummyBundle\DependencyInjection\Compiler\AddExtraDebugDataCompilerPass;

class DummyBundle extends AbstractBundle
{
    public function build(ContainerBuilder $container): void
    {
        parent::build($container);

        $container->addCompilerPass(new AddExtraDebugDataCompilerPass());
    }
}

Do not forget to clear your cache before visualizing the result. We can see that de "Message" entry value as been updated by the CompilerPass.

Symfony debug toolbar with extra data from CompilerPass

You can see the CompilerPass intervention on this schema :

Symfony compilation cycle schema

Github logo See HelloWorldBundle. Github logo See DummyBundle.

To go further

The Symfony dependencies injection is more powerful than the example used in this article where we are solely injection static data.

We also could have used a tag system to add services which compute informations dynamically to display them in the debug toolbar.

A CompilerPass also may be used to decorate services, that is adding them logics without modifying the source code, or also to modify the services definitions depending on the environment.

Conclusion

In this article, we unravelled the three basis of a Symfony app.

First, the compilation cycle, activated on empty app cache. The Kernel injects the ContainerBuilder to all the bundles which provide their CompilerPass list to the ServiceContainer. Then, the services definitions consistency is checked, with the services modifications applied by the CompilerPass. Finally, everything is dump in the cache.

Next, the ServiceContainer which knows every services definitions and instanciate them as needed.

Finally, a CompilerPass usage example for the dynamic modification of services.