Compiling the Container
The service container can be compiled for various reasons. These reasons include checking for any potential issues such as circular references and making the container more efficient by resolving parameters and removing unused services. Also, certain features - like using parent services - require the container to be compiled.
It is compiled by running:
1
$container->compile();
The compile method uses Compiler Passes for the compilation. The DependencyInjection component comes with several passes which are automatically registered for compilation. For example the CheckDefinitionValidityPass checks for various potential issues with the definitions that have been set in the container. After this and several other passes that check the container's validity, further compiler passes are used to optimize the configuration before it is cached. For example, private services and abstract services are removed and aliases are resolved.
Managing Configuration with Extensions
As well as loading configuration directly into the container as shown in The DependencyInjection Component, you can manage it by registering extensions with the container. The first step in the compilation process is to load configuration from any extension classes registered with the container. Unlike the configuration loaded directly, they are only processed when the container is compiled. If your application is modular then extensions allow each module to register and manage their own service configuration.
The extensions must implement ExtensionInterface and can be registered with the container with:
1
$container->registerExtension($extension);
The main work of the extension is done in the load()
method. In the load()
method you can load configuration from one or more configuration files as
well as manipulate the container definitions using the methods shown in
How to work with Service Definition Objects.
The load()
method is passed a fresh container to set up, which is then
merged afterwards into the container it is registered with. This allows
you to have several extensions managing container definitions independently.
The extensions do not add to the containers configuration when they are
added but are processed when the container's compile()
method is called.
A very simple extension may just load configuration files into the container:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
class AcmeDemoExtension implements ExtensionInterface
{
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new XmlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.xml');
}
// ...
}
This does not gain very much compared to loading the file directly into the overall container being built. It just allows the files to be split up among the modules/bundles. Being able to affect the configuration of a module from configuration files outside of the module/bundle is needed to make a complex application configurable. This can be done by specifying sections of config files loaded directly into the container as being for a particular extension. These sections on the config will not be processed directly by the container but by the relevant Extension.
The Extension must specify a getAlias()
method to implement the interface:
1 2 3 4 5 6 7 8 9 10 11
// ...
class AcmeDemoExtension implements ExtensionInterface
{
// ...
public function getAlias(): string
{
return 'acme_demo';
}
}
For YAML configuration files specifying the alias for the extension as a
key will mean that those values are passed to the Extension's load()
method:
1 2 3 4
# ...
acme_demo:
foo: fooValue
bar: barValue
If this file is loaded into the configuration then the values in it are only processed when the container is compiled at which point the Extensions are loaded:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
$container = new ContainerBuilder();
$container->registerExtension(new AcmeDemoExtension);
$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('config.yaml');
// ...
$container->compile();
Note
When loading a config file that uses an extension alias as a key, the extension must already have been registered with the container builder or an exception will be thrown.
The values from those sections of the config files are passed into the first
argument of the load()
method of the extension:
1 2 3 4 5
public function load(array $configs, ContainerBuilder $container): void
{
$foo = $configs[0]['foo']; //fooValue
$bar = $configs[0]['bar']; //barValue
}
The $configs
argument is an array containing each different config file
that was loaded into the container. You are only loading a single config
file in the above example but it will still be within an array. The array
will look like this:
1 2 3 4 5 6
[
[
'foo' => 'fooValue',
'bar' => 'barValue',
],
]
While you can manually manage merging the different files, it is much better to use the Config component to merge and validate the config values. Using the configuration processing you could access the config value this way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Component\Config\Definition\Processor;
// ...
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$processor = new Processor();
$config = $processor->processConfiguration($configuration, $configs);
$foo = $config['foo']; //fooValue
$bar = $config['bar']; //barValue
// ...
}
There are a further two methods you must implement. One to return the XML namespace so that the relevant parts of an XML config file are passed to the extension. The other to specify the base path to XSD files to validate the XML configuration:
1 2 3 4 5 6 7 8 9
public function getXsdValidationBasePath(): string
{
return __DIR__.'/../Resources/config/';
}
public function getNamespace(): string
{
return 'http://www.example.com/symfony/schema/';
}
Note
XSD validation is optional, returning false
from the getXsdValidationBasePath()
method will disable it.
The XML version of the config would then look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:acme-demo="http://www.example.com/schema/dic/acme_demo"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://www.example.com/schema/dic/acme_demo
https://www.example.com/schema/dic/acme_demo/acme_demo-1.0.xsd"
>
<acme-demo:config>
<acme_demo:foo>fooValue</acme_demo:foo>
<acme_demo:bar>barValue</acme_demo:bar>
</acme-demo:config>
</container>
Note
In the Symfony full-stack Framework there is a base Extension class which implements these methods as well as a shortcut method for processing the configuration. See How to Load Service Configuration inside a Bundle for more details.
The processed config value can now be added as container parameters as if
it were listed in a parameters
section of the config file but with the
additional benefit of merging multiple files and validation of the configuration:
1 2 3 4 5 6 7 8 9 10
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$processor = new Processor();
$config = $processor->processConfiguration($configuration, $configs);
$container->setParameter('acme_demo.FOO', $config['foo']);
// ...
}
More complex configuration requirements can be catered for in the Extension classes. For example, you may choose to load a main service configuration file but also load a secondary one only if a certain parameter is set:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$processor = new Processor();
$config = $processor->processConfiguration($configuration, $configs);
$loader = new XmlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.xml');
if ($config['advanced']) {
$loader->load('advanced.xml');
}
}
You can also deprecate container parameters in your extension to warn users about not using them anymore. This helps with the migration across major versions of an extension.
Deprecation is only possible when using PHP to configure the extension, not when
using XML or YAML. Use the ContainerBuilder::deprecateParameter()
method to
provide the deprecation details:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public function load(array $configs, ContainerBuilder $containerBuilder)
{
// ...
$containerBuilder->setParameter('acme_demo.database_user', $configs['db_user']);
$containerBuilder->deprecateParameter(
'acme_demo.database_user',
'acme/database-package',
'1.3',
// optionally you can set a custom deprecation message
'"acme_demo.database_user" is deprecated, you should configure database credentials with the "acme_demo.database_dsn" parameter instead.'
);
}
The parameter being deprecated must be set before being declared as deprecated. Otherwise a ParameterNotFoundException exception will be thrown.
Note
Just registering an extension with the container is not enough to get it included in the processed extensions when the container is compiled. Loading config which uses the extension's alias as a key as in the above examples will ensure it is loaded. The container builder can also be told to load it with its loadFromExtension() method:
1 2 3 4 5 6 7
use Symfony\Component\DependencyInjection\ContainerBuilder;
$container = new ContainerBuilder();
$extension = new AcmeDemoExtension();
$container->registerExtension($extension);
$container->loadFromExtension($extension->getAlias());
$container->compile();
Note
If you need to manipulate the configuration loaded by an extension then you cannot do it from another extension as it uses a fresh container. You should instead use a compiler pass which works with the full container after the extensions have been processed.
Prepending Configuration Passed to the Extension
An Extension can prepend the configuration of any Bundle before the load()
method is called by implementing
PrependExtensionInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
// ...
class AcmeDemoExtension implements ExtensionInterface, PrependExtensionInterface
{
// ...
public function prepend(ContainerBuilder $container): void
{
// ...
$container->prependExtensionConfig($name, $config);
// ...
}
}
For more details, see How to Simplify Configuration of Multiple Bundles, which is specific to the Symfony Framework, but contains more details about this feature.
Execute Code During Compilation
You can also execute custom code during compilation by writing your own
compiler pass. By implementing
CompilerPassInterface
in your extension, the added process()
method will be called during
compilation:
1 2 3 4 5 6 7 8 9 10 11 12
// ...
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
class AcmeDemoExtension implements ExtensionInterface, CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
// ... do something during the compilation
}
// ...
}
As process()
is called after all extensions are loaded, it allows you to
edit service definitions of other extensions as well as retrieving information
about service definitions.
The container's parameters and definitions can be manipulated using the methods described in How to work with Service Definition Objects.
Note
Please note that the process()
method in the extension class is
called during the PassConfig::TYPE_BEFORE_OPTIMIZATION
step. You can read
the next section if you
need to edit the container during another step.
Note
As a rule, only work with services definition 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
Make sure your compiler pass does not require services to exist. Abort the method call if some required service is not available.
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 other services. See the section on service tags for an example.
Creating Separate Compiler Passes
Sometimes, you need to do more than one thing during compilation, want to use
compiler passes without an extension or you need to execute some code at
another step in the compilation process. In these cases, you can create a new
class implementing the CompilerPassInterface
:
1 2 3 4 5 6 7 8 9 10
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
}
}
You then need to register your custom pass with the container:
1 2 3 4
use Symfony\Component\DependencyInjection\ContainerBuilder;
$container = new ContainerBuilder();
$container->addCompilerPass(new CustomPass());
Note
Compiler passes are registered differently if you are using the full-stack framework, see How to Work with Compiler Passes for more details.
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 3 4 5
// ...
$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
);
Dumping the Configuration for Performance
Using configuration files to manage the service container can be much easier
to understand than using PHP once there are a lot of services. This ease
comes at a price though when it comes to performance as the config files
need to be parsed and the PHP configuration built from them. The compilation
process makes the container more efficient but it takes time to run. You
can have the best of both worlds though by using configuration files and
then dumping and caching the resulting configuration. The PhpDumper
serves at dumping the compiled container:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
$file = __DIR__ .'/cache/container.php';
if (file_exists($file)) {
require_once $file;
$container = new ProjectServiceContainer();
} else {
$container = new ContainerBuilder();
// ...
$container->compile();
$dumper = new PhpDumper($container);
file_put_contents($file, $dumper->dump());
}
Tip
The file_put_contents()
function is not atomic. That could cause issues
in a production environment with multiple concurrent requests. Instead, use
the dumpFile() method from Symfony Filesystem
component or other methods provided by Symfony (e.g. $containerConfigCache->write()
)
which are atomic.
ProjectServiceContainer
is the default name given to the dumped container
class. However, you can change this with the class
option when you
dump it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// ...
$file = __DIR__ .'/cache/container.php';
if (file_exists($file)) {
require_once $file;
$container = new MyCachedContainer();
} else {
$container = new ContainerBuilder();
// ...
$container->compile();
$dumper = new PhpDumper($container);
file_put_contents(
$file,
$dumper->dump(['class' => 'MyCachedContainer'])
);
}
You will now get the speed of the PHP configured container with the ease of using configuration files. Additionally dumping the container in this way further optimizes how the services are created by the container.
In the above example you will need to delete the cached container file whenever you make any changes. Adding a check for a variable that determines if you are in debug mode allows you to keep the speed of the cached container in production but getting an up to date configuration whilst developing your application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// ...
// based on something in your project
$isDebug = ...;
$file = __DIR__ .'/cache/container.php';
if (!$isDebug && file_exists($file)) {
require_once $file;
$container = new MyCachedContainer();
} else {
$container = new ContainerBuilder();
// ...
$container->compile();
if (!$isDebug) {
$dumper = new PhpDumper($container);
file_put_contents(
$file,
$dumper->dump(['class' => 'MyCachedContainer'])
);
}
}
This could be further improved by only recompiling the container in debug mode when changes have been made to its configuration rather than on every request. This can be done by caching the resource files used to configure the container in the way described in "Caching based on Resources" in the config component documentation.
You do not need to work out which files to cache as the container builder keeps track of all the resources used to configure it, not just the configuration files but the extension classes and compiler passes as well. This means that any changes to any of these files will invalidate the cache and trigger the container being rebuilt. You need to ask the container for these resources and use them as metadata for the cache:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// ...
// based on something in your project
$isDebug = ...;
$file = __DIR__ .'/cache/container.php';
$containerConfigCache = new ConfigCache($file, $isDebug);
if (!$containerConfigCache->isFresh()) {
$container = new ContainerBuilder();
// ...
$container->compile();
$dumper = new PhpDumper($container);
$containerConfigCache->write(
$dumper->dump(['class' => 'MyCachedContainer']),
$container->getResources()
);
}
require_once $file;
$container = new MyCachedContainer();
Now the cached dumped container is used regardless of whether debug mode
is on or not. The difference is that the ConfigCache
is set to debug
mode with its second constructor argument. When the cache is not in debug
mode the cached container will always be used if it exists. In debug mode,
an additional metadata file is written with all the involved resource
files. These are then checked to see if their timestamps have changed, if they
have the cache will be considered stale.
Note
In the full-stack framework the compilation and caching of the container is taken care of for you.