1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\DependencyInjection\Compiler;
13
14use Symfony\Component\DependencyInjection\ContainerBuilder;
15use Symfony\Component\DependencyInjection\Exception\LogicException;
16use Symfony\Component\DependencyInjection\Exception\RuntimeException;
17use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
18use Symfony\Component\DependencyInjection\Extension\Extension;
19use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
20use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
21use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
22use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
23
24/**
25 * Merges extension configs into the container builder.
26 *
27 * @author Fabien Potencier <fabien@symfony.com>
28 */
29class MergeExtensionConfigurationPass implements CompilerPassInterface
30{
31    /**
32     * {@inheritdoc}
33     */
34    public function process(ContainerBuilder $container)
35    {
36        $parameters = $container->getParameterBag()->all();
37        $definitions = $container->getDefinitions();
38        $aliases = $container->getAliases();
39        $exprLangProviders = $container->getExpressionLanguageProviders();
40
41        foreach ($container->getExtensions() as $extension) {
42            if ($extension instanceof PrependExtensionInterface) {
43                $extension->prepend($container);
44            }
45        }
46
47        foreach ($container->getExtensions() as $name => $extension) {
48            if (!$config = $container->getExtensionConfig($name)) {
49                // this extension was not called
50                continue;
51            }
52            $resolvingBag = $container->getParameterBag();
53            if ($resolvingBag instanceof EnvPlaceholderParameterBag && $extension instanceof Extension) {
54                // create a dedicated bag so that we can track env vars per-extension
55                $resolvingBag = new MergeExtensionConfigurationParameterBag($resolvingBag);
56            }
57            $config = $resolvingBag->resolveValue($config);
58
59            try {
60                $tmpContainer = new MergeExtensionConfigurationContainerBuilder($extension, $resolvingBag);
61                $tmpContainer->setResourceTracking($container->isTrackingResources());
62                $tmpContainer->addObjectResource($extension);
63                if ($extension instanceof ConfigurationExtensionInterface && null !== $configuration = $extension->getConfiguration($config, $tmpContainer)) {
64                    $tmpContainer->addObjectResource($configuration);
65                }
66
67                foreach ($exprLangProviders as $provider) {
68                    $tmpContainer->addExpressionLanguageProvider($provider);
69                }
70
71                $extension->load($config, $tmpContainer);
72            } catch (\Exception $e) {
73                if ($resolvingBag instanceof MergeExtensionConfigurationParameterBag) {
74                    $container->getParameterBag()->mergeEnvPlaceholders($resolvingBag);
75                }
76
77                throw $e;
78            }
79
80            if ($resolvingBag instanceof MergeExtensionConfigurationParameterBag) {
81                // don't keep track of env vars that are *overridden* when configs are merged
82                $resolvingBag->freezeAfterProcessing($extension, $tmpContainer);
83            }
84
85            $container->merge($tmpContainer);
86            $container->getParameterBag()->add($parameters);
87        }
88
89        $container->addDefinitions($definitions);
90        $container->addAliases($aliases);
91    }
92}
93
94/**
95 * @internal
96 */
97class MergeExtensionConfigurationParameterBag extends EnvPlaceholderParameterBag
98{
99    private $processedEnvPlaceholders;
100
101    public function __construct(parent $parameterBag)
102    {
103        parent::__construct($parameterBag->all());
104        $this->mergeEnvPlaceholders($parameterBag);
105    }
106
107    public function freezeAfterProcessing(Extension $extension, ContainerBuilder $container)
108    {
109        if (!$config = $extension->getProcessedConfigs()) {
110            // Extension::processConfiguration() wasn't called, we cannot know how configs were merged
111            return;
112        }
113        $this->processedEnvPlaceholders = [];
114
115        // serialize config and container to catch env vars nested in object graphs
116        $config = serialize($config).serialize($container->getDefinitions()).serialize($container->getAliases()).serialize($container->getParameterBag()->all());
117
118        foreach (parent::getEnvPlaceholders() as $env => $placeholders) {
119            foreach ($placeholders as $placeholder) {
120                if (false !== stripos($config, $placeholder)) {
121                    $this->processedEnvPlaceholders[$env] = $placeholders;
122                    break;
123                }
124            }
125        }
126    }
127
128    /**
129     * {@inheritdoc}
130     */
131    public function getEnvPlaceholders()
132    {
133        return null !== $this->processedEnvPlaceholders ? $this->processedEnvPlaceholders : parent::getEnvPlaceholders();
134    }
135}
136
137/**
138 * A container builder preventing using methods that wouldn't have any effect from extensions.
139 *
140 * @internal
141 */
142class MergeExtensionConfigurationContainerBuilder extends ContainerBuilder
143{
144    private $extensionClass;
145
146    public function __construct(ExtensionInterface $extension, ParameterBagInterface $parameterBag = null)
147    {
148        parent::__construct($parameterBag);
149
150        $this->extensionClass = \get_class($extension);
151    }
152
153    /**
154     * {@inheritdoc}
155     */
156    public function addCompilerPass(CompilerPassInterface $pass, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION/*, int $priority = 0*/)
157    {
158        throw new LogicException(sprintf('You cannot add compiler pass "%s" from extension "%s". Compiler passes must be registered before the container is compiled.', \get_class($pass), $this->extensionClass));
159    }
160
161    /**
162     * {@inheritdoc}
163     */
164    public function registerExtension(ExtensionInterface $extension)
165    {
166        throw new LogicException(sprintf('You cannot register extension "%s" from "%s". Extensions must be registered before the container is compiled.', \get_class($extension), $this->extensionClass));
167    }
168
169    /**
170     * {@inheritdoc}
171     */
172    public function compile($resolveEnvPlaceholders = false)
173    {
174        throw new LogicException(sprintf('Cannot compile the container in extension "%s".', $this->extensionClass));
175    }
176
177    /**
178     * {@inheritdoc}
179     */
180    public function resolveEnvPlaceholders($value, $format = null, array &$usedEnvs = null)
181    {
182        if (true !== $format || !\is_string($value)) {
183            return parent::resolveEnvPlaceholders($value, $format, $usedEnvs);
184        }
185
186        $bag = $this->getParameterBag();
187        $value = $bag->resolveValue($value);
188
189        if (!$bag instanceof EnvPlaceholderParameterBag) {
190            return parent::resolveEnvPlaceholders($value, $format, $usedEnvs);
191        }
192
193        foreach ($bag->getEnvPlaceholders() as $env => $placeholders) {
194            if (false === strpos($env, ':')) {
195                continue;
196            }
197            foreach ($placeholders as $placeholder) {
198                if (false !== stripos($value, $placeholder)) {
199                    throw new RuntimeException(sprintf('Using a cast in "env(%s)" is incompatible with resolution at compile time in "%s". The logic in the extension should be moved to a compiler pass, or an env parameter with no cast should be used instead.', $env, $this->extensionClass));
200                }
201            }
202        }
203
204        return parent::resolveEnvPlaceholders($value, $format, $usedEnvs);
205    }
206}
207