Skip to content

Compiling and Extending the Container

Edit this page

The service container doesn't build the services directly from the configuration files. First, it compiles the whole configuration, loaded from your config/ files and from the extensions of the enabled bundles: it checks for potential issues such as circular references, resolves parameters, removes unused services and optimizes the definitions to make the container as fast as possible. Then, it caches the compiled container in var/cache/ to avoid repeating all this work on every request.

The compilation runs as a series of steps called compiler passes. Symfony includes many passes that perform all the checks and optimizations, and you can create your own passes to modify the services in any way you need (add or remove services, replace arguments, add tags or aliases, decorate services, etc.) before the application starts using them.

Creating a Compiler Pass

A compiler pass is a class implementing CompilerPassInterface. Its process() method receives the container builder so it can manipulate services and parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/DependencyInjection/Compiler/CustomPass.php
namespace App\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class CustomPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        // ... do something during the compilation
    }
}

Working with Service Definitions

A service definition is an object that describes how the container must create a service: its class, its constructor arguments, its tags, whether it's lazy or shared, etc. No matter how you configure your services (automatic discovery, PHP attributes or YAML and PHP files), the container turns all that configuration into Definition objects. The actual services are only instantiated later, when the application asks for them.

Compiler passes and container extensions work in that intermediate step: they inspect and modify the Definition objects with PHP code, so they can change anything about the services before they are created. In the following examples, the $container variable is the ContainerBuilder object that the process() method of the compiler pass receives, as shown in the previous section.

Getting and Setting Service Definitions

There are some helpful methods for working with the service definitions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\DependencyInjection\Definition;

// finds out if there is an "app.mailer" definition
$container->hasDefinition('app.mailer');
// finds out if there is an "app.mailer" definition or alias
$container->has('app.mailer');

// gets the "app.user_config_loader" definition
$definition = $container->getDefinition('app.user_config_loader');
// gets the definition with the "app.user_config_loader" ID or alias
$definition = $container->findDefinition('app.user_config_loader');

// adds a new "app.number_generator" definition
$definition = new Definition(\App\NumberGenerator::class);
$container->setDefinition('app.number_generator', $definition);

// shortcut for the previous method
$container->register('app.number_generator', \App\NumberGenerator::class);

Note

As a rule, only work with service definitions in a compiler pass and do not create service instances. In practice, this means using the methods has(), findDefinition(), getDefinition(), setDefinition(), etc. instead of get(), set(), etc.

Tip

Don't assume that the services manipulated by your compiler pass are always defined in the container: they might be registered only in some config environments or only when certain bundles are installed. Asking for a definition that doesn't exist throws an exception that breaks the compilation of the whole container. That's why, if some required service might not exist, check it first with the $container->has() method and end the process() method early with a return statement, as shown in this example.

Setting the Definition Class

The first optional argument of the Definition class is the fully qualified class name of the object returned when the service is fetched from the container:

1
2
3
4
5
6
7
8
9
10
11
use App\Config\CustomConfigLoader;
use App\Config\UserConfigLoader;
use Symfony\Component\DependencyInjection\Definition;

$definition = new Definition(UserConfigLoader::class);

// override the class
$definition->setClass(CustomConfigLoader::class);

// get the class configured for this definition
$class = $definition->getClass();

Setting the Constructor Arguments

The second optional argument of the Definition class is an array with the arguments passed to the constructor of the object returned when the service is fetched from the container:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use App\Config\DoctrineConfigLoader;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

$definition = new Definition(DoctrineConfigLoader::class, [
    new Reference('doctrine'), // a reference to another service
    '%app.config_table_name%',  // will be resolved to the value of a container parameter
]);

// gets all arguments configured for this definition
$constructorArguments = $definition->getArguments();

// gets a specific argument
$firstArgument = $definition->getArgument(0);

// adds a new named argument
// '$argumentName' = the name of the argument in the constructor, including the '$' symbol
$definition = $definition->setArgument('$argumentName', $argumentValue);

// adds a new argument
$definition->addArgument($argumentValue);

// replaces argument on a specific index (0 = first argument)
$definition->replaceArgument($index, $argument);

// replaces all previously configured arguments with the passed array
$definition->setArguments($arguments);

Warning

Don't use get() to get a service that you want to inject as constructor argument, the service is not yet available. Instead, use a Reference instance as shown above.

Setting the Method Calls

If the service you are working with uses setter injection then you can manipulate any method calls in the definitions as well:

1
2
3
4
5
6
7
8
9
10
11
// gets all configured method calls
$methodCalls = $definition->getMethodCalls();

// configures a new method call
$definition->addMethodCall('setLogger', [new Reference('logger')]);

// configures an immutable-setter
$definition->addMethodCall('withLogger', [new Reference('logger')], true);

// replaces all previously configured method calls with the passed array
$definition->setMethodCalls($methodCalls);

Tip

There are more examples of specific ways of working with definitions in the PHP code blocks of the Service Container articles such as Using a Factory to Create Services and parent services.

Note

The methods here that change service definitions can only be used before the container is compiled. Once the container is compiled you cannot manipulate service definitions further.

Processing Tagged Services in a Compiler Pass

A common use case of compiler passes is to search for all service definitions that have a certain tag in order to dynamically plug each one into another service.

Tip

If you only need to inject the collection of tagged services somewhere, use a tagged iterator instead; it doesn't require writing a compiler pass.

Suppose your application generates reports in several formats and that you want to register each report generator into a central manager class calling its addGenerator() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Report/ReportGeneratorRegistry.php
namespace App\Report;

class ReportGeneratorRegistry
{
    private array $generators = [];

    public function addGenerator(ReportGeneratorInterface $generator, string $format): void
    {
        $this->generators[$format] = $generator;
    }

    public function generate(string $format, array $data): string
    {
        // ...
    }
}

Tag all the generator services with a custom tag (app.report_generator) that defines a format attribute (the recommended way is the #[AutoconfigureTag] attribute on the common interface; see autoconfiguring tags and tag attributes). Then, write a compiler pass that asks the container for the tagged services with the findTaggedServiceIds() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/DependencyInjection/Compiler/ReportGeneratorPass.php
namespace App\DependencyInjection\Compiler;

use App\Report\ReportGeneratorRegistry;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class ReportGeneratorPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        // always first check if the primary service is defined
        if (!$container->has(ReportGeneratorRegistry::class)) {
            return;
        }

        $definition = $container->findDefinition(ReportGeneratorRegistry::class);

        // find all service IDs with the app.report_generator tag
        $taggedServices = $container->findTaggedServiceIds('app.report_generator');

        foreach ($taggedServices as $id => $tags) {
            // a service could have the same tag twice
            foreach ($tags as $attributes) {
                $definition->addMethodCall('addGenerator', [
                    new Reference($id),
                    $attributes['format'],
                ]);
            }
        }
    }
}

The double loop may be confusing. This is because a service can have more than one tag. You tag a service twice or more with the app.report_generator tag. The second foreach loop iterates over the app.report_generator tags set for the current service and gives you the attributes.

Creating Service Locators in a Compiler Pass

Sometimes, instead of injecting all the collected services into the main service, you want to inject them via a service locator so they are only instantiated when used. Create the locator with the register() method: it saves you some boilerplate and it shares identical locators among all the services referencing them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

public function process(ContainerBuilder $container): void
{
    // ...

    $locateableServices = [
        // ...
        'logger' => new Reference('logger'),
    ];

    $myService = $container->findDefinition(MyService::class);

    $myService->addArgument(ServiceLocatorTagPass::register($container, $locateableServices));
}

Registering the Compiler Pass

Once the compiler pass is created, register it in the container so it runs during the compilation. The way to do it depends on the type of application.

Registering the Pass in a Symfony Application

In Symfony applications, enable your compiler passes in the build() method of the application kernel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Kernel.php
namespace App;

use App\DependencyInjection\Compiler\CustomPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    // ...

    protected function build(ContainerBuilder $container): void
    {
        $container->addCompilerPass(new CustomPass());
    }
}

If your compiler pass is relatively small, you can define it inside the application's Kernel class instead of creating a separate compiler pass class. To do so, make your kernel implement CompilerPassInterface and add the compiler pass code inside the process() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Kernel.php
namespace App;

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel implements CompilerPassInterface
{
    use MicroKernelTrait;

    // ...

    public function process(ContainerBuilder $container): void
    {
        // in this method you can manipulate the service container:
        // for example, changing some container service:
        $container->getDefinition('app.some_private_service')->setPublic(true);

        // or processing tagged services:
        foreach ($container->findTaggedServiceIds('some_tag') as $id => $tags) {
            // ...
        }
    }
}

Registering the Pass in a Bundle

Bundles can define compiler passes in the build() method of the main bundle class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/MyBundle/MyBundle.php
namespace App\MyBundle;

use App\DependencyInjection\Compiler\CustomPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class MyBundle extends AbstractBundle
{
    public function build(ContainerBuilder $container): void
    {
        $container->addCompilerPass(new CustomPass());
    }
}

If your compiler pass is relatively small, you can make the main bundle class implement CompilerPassInterface and it will be automatically registered as a compiler pass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/MyBundle/MyBundle.php
namespace App\MyBundle;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class MyBundle extends AbstractBundle implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        // in this method you can manipulate the service container:
        // for example, changing some container service:
        $container->getDefinition('app.some_private_service')->setPublic(true);

        // or processing tagged services:
        foreach ($container->findTaggedServiceIds('some_tag') as $id => $tags) {
            // ...
        }
    }
}

8.1

Support for using bundle classes as compiler passes was introduced in Symfony 8.1. Previously, you had to register the bundle class itself by calling $container->addCompilerPass($this); in the build() method.

If you are using custom service tags in a bundle, the convention is to format tag names by starting with the bundle's name in lowercase (using underscores as separators), followed by a dot, and finally the specific tag name. For example, to introduce a "transport" tag in your AcmeMailerBundle, you would name it acme_mailer.transport.

Registering the Pass in a Standalone Application

When using the container in standalone PHP applications, register the pass with the addCompilerPass() method before compiling the container:

1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->addCompilerPass(new CustomPass());

// ...
$container->compile();

Controlling the Pass Ordering

The default compiler passes are grouped into optimization passes and removal passes. The optimization passes run first and include tasks such as resolving references within the definitions. The removal passes perform tasks such as removing private aliases and unused services. When registering compiler passes using addCompilerPass(), you can configure when your compiler pass is run. By default, they are run before the optimization passes.

You can use the following constants to determine when your pass is executed:

  • PassConfig::TYPE_BEFORE_OPTIMIZATION
  • PassConfig::TYPE_OPTIMIZE
  • PassConfig::TYPE_BEFORE_REMOVING
  • PassConfig::TYPE_REMOVE
  • PassConfig::TYPE_AFTER_REMOVING

For example, to run your custom pass after the default removal passes have been run, use:

1
2
// ...
$container->addCompilerPass(new CustomPass(), PassConfig::TYPE_AFTER_REMOVING);

You can also control the order in which compiler passes are run for each compilation phase. Use the optional third argument of addCompilerPass() to set the priority as an integer number. The default priority is 0 and the higher its value, the earlier it's executed:

1
2
3
4
5
6
7
8
// ...
// FirstPass is executed after SecondPass because its priority is lower
$container->addCompilerPass(
    new FirstPass(), PassConfig::TYPE_AFTER_REMOVING, 10
);
$container->addCompilerPass(
    new SecondPass(), PassConfig::TYPE_AFTER_REMOVING, 30
);

Registering Autoconfiguration Rules from PHP Code

Besides the autoconfiguration options available in attributes and configuration files, you can define autoconfiguration rules programmatically with the registerForAutoconfiguration() method.

In a Symfony application, call this method in your kernel class:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Kernel.php
class Kernel extends BaseKernel
{
    // ...

    protected function build(ContainerBuilder $container): void
    {
        $container->registerForAutoconfiguration(CustomInterface::class)
            ->addTag('app.custom_tag')
        ;
    }
}

In bundles extending the AbstractBundle class, call this method in the loadExtension() method of the main bundle class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class MyBundle extends AbstractBundle
{
    public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
    {
        $builder
            ->registerForAutoconfiguration(CustomInterface::class)
            ->addTag('app.custom_tag')
        ;
    }
}

Note

For bundles not extending the AbstractBundle class, call this method in the load() method of the bundle extension class.

Autoconfiguration registering is not limited to interfaces. It is possible to use PHP attributes to autoconfigure services by using the registerAttributeForAutoconfiguration() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/Attribute/SensitiveElement.php
namespace App\Attribute;

#[\Attribute(\Attribute::TARGET_CLASS)]
class SensitiveElement
{
    public function __construct(
        private string $token,
    ) {
    }

    public function getToken(): string
    {
        return $this->token;
    }
}

// src/Kernel.php
use App\Attribute\SensitiveElement;

class Kernel extends BaseKernel
{
    // ...

    protected function build(ContainerBuilder $container): void
    {
        // ...

        $container->registerAttributeForAutoconfiguration(SensitiveElement::class, static function (ChildDefinition $definition, SensitiveElement $attribute, \ReflectionClass $reflector): void {
            // Apply the 'app.sensitive_element' tag to all classes with SensitiveElement
            // attribute, and attach the token value to the tag
            $definition->addTag('app.sensitive_element', ['token' => $attribute->getToken()]);
        });
    }
}

You can also make attributes usable on methods. To do so, update the previous example and add Attribute::TARGET_METHOD:

1
2
3
4
5
6
7
8
// src/Attribute/SensitiveElement.php
namespace App\Attribute;

#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
class SensitiveElement
{
    // ...
}

Then, update the registerAttributeForAutoconfiguration() call to support ReflectionMethod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Kernel.php
use App\Attribute\SensitiveElement;

class Kernel extends BaseKernel
{
    // ...

    protected function build(ContainerBuilder $container): void
    {
        // ...

        $container->registerAttributeForAutoconfiguration(SensitiveElement::class, static function (
            ChildDefinition $definition,
            SensitiveElement $attribute,
            // update the union type to support multiple types of reflection
            // you can also use the "\Reflector" interface
            \ReflectionClass|\ReflectionMethod $reflector): void {
                if ($reflector instanceof \ReflectionMethod) {
                    // ...
                }
            }
        );
    }
}

Tip

You can also define an attribute to be usable on properties and parameters with Attribute::TARGET_PROPERTY and Attribute::TARGET_PARAMETER; then support ReflectionProperty and ReflectionParameter in your registerAttributeForAutoconfiguration() callable.

Resolving Environment Variables at Compile Time

Warning

This practice is discouraged. Use it only if you fully understand the implications.

By default, environment variables are resolved at runtime. However, you can force their resolution at compile time using the following code:

1
2
3
4
$parameterValue = $container->resolveEnvPlaceholders(
    $container->getParameter('%env(ENV_VAR_NAME)%'),
    true // resolve to actual values
);

However, a major drawback of this approach is that you must manually clear the cache when changing the value of an environment variable. This goes against the typical behavior of environment variables, which are designed to be dynamic and not require cache invalidation.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version