Skip to content

[bundle] Tricky (unwanted?) configuration behaviour #634

Closed
@Gnucki

Description

@Gnucki

I am trying to migrate from Sf4.1 to 4.2 so from enqueue 0.8 to 0.9.
I found a tricky behaviour with configuration that I am going to try to explain.

I have a bundle handling messaging for my apps where I define some enqueue config in the bundle:

enqueue:
    async_events:
        enabled: false
    transport:
        default:
            dsn: '%env(BROKER_DSN)%'
    client: ~

That I inject to enqueue extension with a prepend in my bundle extension:

class AsyncExtension extends Extension implements PrependExtensionInterface
{
    const CONFIG_PATH = __DIR__.'/../Resources/config';

    /**
     * {@inheritdoc}
     */
    public function prepend(ContainerBuilder $container)
    {
        $bundles = $container->getParameter('kernel.bundles');

        foreach ($container->getExtensions() as $name => $extension) {
            switch ($name) {
                case 'enqueue':
                    $config = Yaml::parseFile(self::CONFIG_PATH.'/enqueue.yaml');
                    $container->prependExtensionConfig($name, $config['enqueue']);
                    break;
                case 'framework':
                    $config = Yaml::parseFile(self::CONFIG_PATH.'/messenger.yaml');
                    $container->prependExtensionConfig($name, $config['framework']);
                    break;
            }
        }
    }

    // ...
}

But that config is never used! Why? Because of a conjunction of things!

First is that code in enqueue-bundle:

// Enqueue\Bundle\DependencyInjection/Configuration

$transportNode = $rootNode->children()->arrayNode('transport');
$transportNode
    ->beforeNormalization()
    ->always(function ($value) {
        if (empty($value)) {
            return ['default' => ['dsn' => 'null:']];
        }
        if (is_string($value)) {
            return ['default' => ['dsn' => $value]];
        }

        if (is_array($value) && array_key_exists('dsn', $value)) {
            return ['default' => $value];
        }

        return $value;
    });

This is defining a standard configuration in case of an empty configuration.

Second, is this code in Symfony kernel:

// Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass

class MergeExtensionConfigurationPass extends BaseMergeExtensionConfigurationPass
{
    private $extensions;

    public function __construct(array $extensions)
    {
        $this->extensions = $extensions;
    }

    public function process(ContainerBuilder $container)
    {
        foreach ($this->extensions as $extension) {
            if (!\count($container->getExtensionConfig($extension))) {
                $container->loadFromExtension($extension, array());
            }
        }

        parent::process($container);
    }
}

This is creating a default empty config for extension with no config. Remember that this is my bundle which is defining a default config with prepend mechanism. There is no config in my app.

However, prepend mechanism happens in parent::process($container); so after this code. This means that, at this point, my config is empty. Then, an empty config is added to the list of config to merge in enqueue-bundle extension:

// Enqueue\Bundle\DependencyInjection\EnqueueExtension

final class EnqueueExtension extends Extension implements PrependExtensionInterface
{
    public function load(array $configs, ContainerBuilder $container): void
    {
        $config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs);
        // ...
    }
}      

The bad news? Prepend add config before others to make it overridable:

// Symfony\Component\DependencyInjection\ContainerBuilder

public function prependExtensionConfig($name, array $config)
{
    if (!isset($this->extensionConfigs[$name])) {
        $this->extensionConfigs[$name] = array();
    }

    array_unshift($this->extensionConfigs[$name], $config);
}

This means that default config created by MergeExtensionConfigurationPass override the one of my bundle at config merge step! Finally, I have:

enqueue:
    async_events:
        enabled: false
    transport:
        default:
            dsn: 'null:'
    client: ~

I hope I have been clear in my explanations. Any thoughts on that?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions