When creating a Symfony bundle, you'll often want to use a custom Configuration
to configure, or even add, various services during the compilation of the service container. Symfony provides this option by implementing an Extension. While an Extension
provides a lot of options to do so, a big configuration can easily create a massive extension class that does way too much.
An alternative to this approach is using Compiler Passes to split up all the logic into separate steps. A big drawback however is that by default these compiler passes don't have access to the final compiled configuration.
To get around this problem, an often used solution is to store (parts of) the configuration in a temporary transient parameter on the service container. Because the compiler pass has access to (a copy) of the container, you can read that parameter and remove it from the container before compiling is completed.
Although this is a viable option, the use of these temporary values always seemed a bit off to me; plus the developer must be very conscious of removing the values afterwards. Let's look at a great alternative option of providing this configuration to the compiler passes.
Injecting the extension into the compiler passes
Because the Extension
is the location where the configurations of that bundle are aggregated and combined, it should serve as the only trustworthy source for the configuration. That means we should only retrieve the configuration from the extension class.
As luck would have it; you initialize and add a compiler pass in the build()
method on the bundle class. This class also has access to the extension, by use of the getContainerExtension()
method. That means we can inject the
extension into our compiler pass like this:
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class MyEpicBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
$extension = $this->getContainerExtension();
if ($extension instanceof MyEpicExtension) {
$container->addCompilerPass(new MyEpicCompilerPass($extension));
}
}
}
To accept the extension in the compiler pass, we need to make sure to receive it as its first argument:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class MyEpicCompilerPass implements CompilerPassInterface
{
public function __construct(private MyEpicExtension $extension)
{
}
public function process(ContainerBuilder $container): void
{
// ... We want our configuration here.
}
}
Retrieving the configuration from the extension
An extension has to implement a load()
method that receives all provided configurations (e.g. from the project or other bundles that need to configure this bundle), as well as a copy of the container. To compile all these configurations into a final configuration array you can either use a Processor
, or use the Extension::processConfiguration()
method if you extend the base Extension
.
In either case, you will probably want to avoid processing the configuration multiple times; so let's add a micro cache that contains the compiled configuration, and retrieves only that.
use Symfony\Component\DependencyInjection\Extension\Extension;
final class MyEpicExtension extends Extension
{
private array $config;
public function load(array $configs, ContainerBuilder $contaienr): void
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config'));
$loader->load('services.yaml');
$this->compileConfig($configs);
}
/**
* Compiles and stores the configuration once.
*/
private function compileConfig(array $configs): void
{
$configuration = new Configuration(); // This is our bundle's configuration class.
$this->config ??= $this->processConfiguration($configuration, $configs);
}
/**
* Retrieves the configuration for the bundle.
*/
public function getConfig(): array
{
return $this->config ?? [];
}
}
Now we are able to retrieve the entire bundle configuration in our Compiler Pass by calling the getConfig()
method on the extension.
// MyEpicCompilerPass.php
public function process(ContainerBuilder $container): void
{
$config = $this->extension->getConfig();
// ...
}
The compiling of the extension configurations is actually done in a compiler pass. This is called the Merge Pass and is the first compiler pass to be processed. Because of this, once our custom compiler pass reads the configuration of off the extension, we can be sure it is already compiled, cached and ready for use.
That's it
As I said; there are alternative ways of using configuration inside a compiler pass. And a compiler pass might not even be necessary in some cases. A small configuration could very well be processed inside the bundle extension. Like always "it depends"; but this too is a nice thing to have in your tool belt.
I hope you enjoyed reading this post! If so, please leave a 👍 reaction or a 💬 comment and consider subscribing to my newsletter! You can also follow me on 🐦 twitter for more content and the occasional tip.
Top comments (0)