Jens
Jens

Reputation: 5877

Can I access semantic configuration in a compiler pass?

I have semantic configuration for a bundle that needs to be interpreted during a compiler pass for the same bundle.

Is it possible to access it without storing it in an intermediate container variable?

Upvotes: 7

Views: 4054

Answers (7)

Alexander Dimitrov
Alexander Dimitrov

Reputation: 974

It is an old question, but I'm using so far this:

use Symfony\Component\Config\Definition\Processor;
...

public function process(ContainerBuilder $container)
{
    $config = $this->getConfiguration($container);
    ...
}

/**
 * @param ContainerBuilder $container
 * @return array
 */
private function getConfiguration(ContainerBuilder $container)
{
    $parameterBag = $container->getParameterBag();
    $processor = new Processor();
    $configuration = new Configuration();

    return $processor->processConfiguration($configuration, $parameterBag->resolveValue($container->getExtensionConfig('YOUR_EXTENSION')));
}

Yes, for every CompilerPass it will process the configuration, but in my case I have only one, so it's not big of a deal..

Upvotes: 1

Will B.
Will B.

Reputation: 18426

I was looking for how to read the processed Configuration, without setting a custom parameter to retrieve later in the CompilerPass and came across the following solution.

Looking at the Kernel's order of operations in Kernel::prepareContainer, we can see that it calls Bundle::getContainerExtension() followed by Bundle::build() This means the Extension::load method is called and the configuration is processed before running the compiler passes.

Since Symfony also registers the extensions in the container, you simply need to retrieve the extension from the container in your CompilerPass.

However since Symfony also empties the processedConfigs array, after the extension is loaded, you need to make the config accessible to the CompilerPass.

namespace AppBundle;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use AppBundle\DependencyInjection\Compiler\AppCompilerPass;

class AppBundle extends Bundle
{
    public function build()
    {
        $container->addCompilerPass(new AppCompilerPass());
    }

    /** demonstration of the default functionality in Bundle
    public function getContainerExtension()
    {
        if (null === $this->extension) {
            $this->extension = new AppBundle\DependencyInjection\AppExtension();
        }

        return $this->extension;
    } */
}
namespace AppBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;

class AppExtension extends Extension
{
    const ALIAS = 'app'; //just for demonstration and reference in CompilerPass

    private $config = array();

    public function load(array $configs, ContainerBuilder $container): void
    {
        //make the processed configuration available
        $this->config = $this->processConfiguration(new Configuration(), $configs);
        //...
    }

    public function getConfig()
    {
        try {
            return $this->config;
        } finally {
            //erases the config after it is retrieved, for security and performance reasons
            $this->config = array();
        }
    }

    /** demonstration of default functionality in Extension
    public function getAlias()
    {
        return self::ALIAS;
    } */
}
namespace AppBundle\DependencyInjection\Compiler;

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

class AppCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        //...
        if (!$container->hasExtension(AppExtension::ALIAS)) {
            //always make sure the extension exists
            return;
        }
        $config = $container->getExtension(AppExtension::ALIAS)->getConfig();
        dump($config);
        //...
    }
}

Instead of retrieving the extension from the container, you can pass the extension to your compiler pass.

class AppBundle extends Bundle
{
    public function build()
    {
        $container->addCompilerPass(new AppCompilerPass($this->getContainerExtension()));
    }
}
use AppBundle\DependencyInjection\AppExtension;

class AppCompilerPass implements CompilerPassInterface
{
    private $extension;

    public function __construct(AppExtension $extension = null)
    {
        $this->extension = $extension;
    }


    public function process(ContainerBuilder $container)
    {
        //...
        if (!$this->extension) {
            //always make sure the extension exists
            return;
        }
        $config = $this->extension->getConfig();
        dump($config);
        //...
    }
}

Upvotes: 4

Gnucki
Gnucki

Reputation: 5133

I'm a bit late but here is how I am used to retrieve config in a compiler pass (this should make everyone happy ;).

First, let's set the config (or a part of it) in a parameter:

<?php

namespace Me\Bundle\MyBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Yaml\Yaml;

class MyBundleExtension extends Extension
{
    const CONFIG_PATH = __DIR__.'/../Resources/config';

    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs);

        $loader = new YamlFileLoader($container, new FileLocator(self::CONFIG_PATH));

        $container->setParameter('my_bundle.config', $config);
    }
}

Then, you can use it like that in your compiler pass:

<?php

namespace Me\Bundle\MyBundle\DependencyInjection\Compiler;

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

class UseConfigPass implements CompilerPassInterface
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        // ...

        $config = $container->getParameter('my_bundle.config');

        // ...
    }
}

This way, config is only processed in extension file and you do not need to ask for the first element!

Note that if you wish to alter configuration of another bundle, you might want to take a look at prepend extension.

Upvotes: 1

Geoffrey P&#233;cro
Geoffrey P&#233;cro

Reputation: 11

@xPheRe It's not a good solution to load the configuration in a compiler pass. It would mean that if you have several compilers, you would load the configuration several times. Configuration should be loaded once in the extension.

So as @Peter said, and if you are sure that the configuration exists, do:

$config = $container->getExtensionConfig('acme_demo')[0];

Then you can do something like that:

$definition = new Definition('your_service_id');
$definition->setArgument(0, $config);

Upvotes: 1

xPheRe
xPheRe

Reputation: 2343

Just for the sake of completeness to @Peter's answer: getExtensionConfig returns an array of arrays which should be processed with the corresponding Configuration to be able to access default values.

<?php

namespace Acme\DemoBundle\DependencyInjection;

use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

class CompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $configs = $container->getExtensionConfig('acme_demo');
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        /// You can safely work with $config now
    }

    private function processConfiguration(ConfigurationInterface $configuration, array $configs)
    {
        $processor = new Processor();

        return $processor->processConfiguration($configuration, $configs);
    }
}

Upvotes: 6

David Patterson
David Patterson

Reputation: 1920

I realize that this is an old post, but I was looking for the same information and eventually found that this works for a single parameter:

$cfgVal = $container
  ->getParameterBag()
  ->resolveValue( $container->getParameter( 'param_name' ));

Of course it's possible that this functionality was added after the original post.

Upvotes: 4

Peter
Peter

Reputation: 31751

Yes, kind of:

<?php

namespace Acme\DemoBundle\DependencyInjection;

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

class CompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $configs = $container->getExtensionConfig('acme_demo');
    }
}

From what I can see $configs is an array of unmerged configurations and default values are not included (values defined by the configuration TreeBuilder).

See here and here

Upvotes: 8

Related Questions