From bac2132aeb449f870f05aed77756801766f69f54 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 12 Aug 2016 20:34:27 +0200 Subject: [PATCH] [DI] Allow injecting ENV parameters at runtime using %env(MY_ENV_VAR)% syntax --- .../DependencyInjection/Compiler/Compiler.php | 26 ++- .../DependencyInjection/Container.php | 33 +++- .../DependencyInjection/ContainerBuilder.php | 76 +++++++++ .../Dumper/GraphvizDumper.php | 2 +- .../DependencyInjection/Dumper/PhpDumper.php | 131 +++++++++++++-- .../DependencyInjection/Dumper/XmlDumper.php | 2 +- .../DependencyInjection/Dumper/YamlDumper.php | 2 +- .../Exception/EnvNotFoundException.php | 25 +++ .../Exception/EnvParameterException.php | 25 +++ .../EnvPlaceholderParameterBag.php | 69 ++++++++ .../ParameterBag/ParameterBag.php | 14 +- .../Tests/ContainerBuilderTest.php | 9 + .../Tests/Dumper/PhpDumperTest.php | 26 +++ .../Tests/Fixtures/php/services10.php | 30 +++- .../Tests/Fixtures/php/services12.php | 42 ++++- .../Tests/Fixtures/php/services26.php | 154 ++++++++++++++++++ .../Tests/Fixtures/php/services9_compiled.php | 30 +++- .../Tests/Fixtures/yaml/services26.yml | 10 ++ src/Symfony/Component/HttpKernel/Kernel.php | 4 +- 19 files changed, 666 insertions(+), 44 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Exception/EnvNotFoundException.php create mode 100644 src/Symfony/Component/DependencyInjection/Exception/EnvParameterException.php create mode 100644 src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services26.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services26.yml diff --git a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php index 5d83a6a8f5e86..cc5d9adc792a5 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\EnvParameterException; /** * This class is used to remove circular dependencies between individual passes. @@ -108,8 +109,29 @@ public function getLog() */ public function compile(ContainerBuilder $container) { - foreach ($this->passConfig->getPasses() as $pass) { - $pass->process($container); + try { + foreach ($this->passConfig->getPasses() as $pass) { + $pass->process($container); + } + } catch (\Exception $e) { + $usedEnvs = array(); + $prev = $e; + + do { + $msg = $prev->getMessage(); + + if ($msg !== $resolvedMsg = $container->resolveEnvPlaceholders($msg, null, $usedEnvs)) { + $r = new \ReflectionProperty($prev, 'message'); + $r->setAccessible(true); + $r->setValue($prev, $resolvedMsg); + } + } while ($prev = $prev->getPrevious()); + + if ($usedEnvs) { + $e = new EnvParameterException($usedEnvs, $e); + } + + throw $e; } } } diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php index 9dd0512aef7c8..7fbfff2291ba9 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php @@ -11,11 +11,12 @@ namespace Symfony\Component\DependencyInjection; +use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; /** @@ -70,13 +71,14 @@ class Container implements ResettableContainerInterface protected $loading = array(); private $underscoreMap = array('_' => '', '.' => '_', '\\' => '_'); + private $envCache = array(); /** * @param ParameterBagInterface $parameterBag A ParameterBagInterface instance */ public function __construct(ParameterBagInterface $parameterBag = null) { - $this->parameterBag = $parameterBag ?: new ParameterBag(); + $this->parameterBag = $parameterBag ?: new EnvPlaceholderParameterBag(); } /** @@ -372,6 +374,33 @@ public static function underscore($id) return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), str_replace('_', '.', $id))); } + /** + * Fetches a variable from the environment. + * + * @param string The name of the environment variable + * + * @return scalar The value to use for the provided environment variable name + * + * @throws EnvNotFoundException When the environment variable is not found and has no default value + */ + protected function getEnv($name) + { + if (isset($this->envCache[$name]) || array_key_exists($name, $this->envCache)) { + return $this->envCache[$name]; + } + if (isset($_ENV[$name])) { + return $this->envCache[$name] = $_ENV[$name]; + } + if (false !== $env = getenv($name)) { + return $this->envCache[$name] = $env; + } + if (!$this->hasParameter("env($name)")) { + throw new EnvNotFoundException($name); + } + + return $this->envCache[$name] = $this->getParameter("env($name)"); + } + private function __clone() { } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 997d06f41ba98..b82465480dd10 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface; @@ -89,6 +90,16 @@ class ContainerBuilder extends Container implements TaggedContainerInterface */ private $usedTags = array(); + /** + * @var string[][] A map of env var names to their placeholders + */ + private $envPlaceholders = array(); + + /** + * @var int[] A map of env vars to their resolution counter. + */ + private $envCounters = array(); + private $compiled = false; /** @@ -481,6 +492,18 @@ public function merge(ContainerBuilder $container) $this->extensionConfigs[$name] = array_merge($this->extensionConfigs[$name], $container->getExtensionConfig($name)); } + + if ($this->getParameterBag() instanceof EnvPlaceholderParameterBag && $container->getParameterBag() instanceof EnvPlaceholderParameterBag) { + $this->getParameterBag()->mergeEnvPlaceholders($container->getParameterBag()); + } + + foreach ($container->envCounters as $env => $count) { + if (!isset($this->envCounters[$env])) { + $this->envCounters[$env] = $count; + } else { + $this->envCounters[$env] += $count; + } + } } /** @@ -551,8 +574,11 @@ public function compile() } $this->extensionConfigs = array(); + $bag = $this->getParameterBag(); parent::compile(); + + $this->envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : array(); } /** @@ -995,6 +1021,56 @@ public function getExpressionLanguageProviders() return $this->expressionLanguageProviders; } + /** + * Resolves env parameter placeholders in a string. + * + * @param string $string The string to resolve + * @param string|null $format A sprintf() format to use as replacement for env placeholders or null to use the default parameter format + * @param array &$usedEnvs Env vars found while resolving are added to this array + * + * @return string The string with env parameters resolved + */ + public function resolveEnvPlaceholders($string, $format = null, array &$usedEnvs = null) + { + $bag = $this->getParameterBag(); + $envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : $this->envPlaceholders; + + if (null === $format) { + $format = '%%env(%s)%%'; + } + + foreach ($envPlaceholders as $env => $placeholders) { + foreach ($placeholders as $placeholder) { + if (false !== stripos($string, $placeholder)) { + $string = str_ireplace($placeholder, sprintf($format, $env), $string); + $usedEnvs[$env] = $env; + $this->envCounters[$env] = isset($this->envCounters[$env]) ? 1 + $this->envCounters[$env] : 1; + } + } + } + + return $string; + } + + /** + * Get statistics about env usage. + * + * @return int[] The number of time each env vars has been resolved + */ + public function getEnvCounters() + { + $bag = $this->getParameterBag(); + $envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : $this->envPlaceholders; + + foreach ($envPlaceholders as $env => $placeholders) { + if (!isset($this->envCounters[$env])) { + $this->envCounters[$env] = 0; + } + } + + return $this->envCounters; + } + /** * Returns the Service Conditionals. * diff --git a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php index c569232f20f1d..09c111a9c6945 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php @@ -81,7 +81,7 @@ public function dump(array $options = array()) } } - return $this->startDot().$this->addNodes().$this->addEdges().$this->endDot(); + return $this->container->resolveEnvPlaceholders($this->startDot().$this->addNodes().$this->addEdges().$this->endDot(), '__ENV_%s__'); } /** diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index d2d567093c5e3..b3a4008369d81 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Parameter; +use Symfony\Component\DependencyInjection\Exception\EnvParameterException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; @@ -98,6 +99,8 @@ public function setProxyDumper(ProxyDumper $proxyDumper) * @param array $options An array of options * * @return string A PHP class representing of the service container + * + * @throws EnvParameterException When an env var exists but has not been dumped */ public function dump(array $options = array()) { @@ -156,6 +159,16 @@ public function dump(array $options = array()) ; $this->targetDirRegex = null; + $unusedEnvs = array(); + foreach ($this->container->getEnvCounters() as $env => $use) { + if (!$use) { + $unusedEnvs[] = $env; + } + } + if ($unusedEnvs) { + throw new EnvParameterException($unusedEnvs); + } + return $code; } @@ -384,7 +397,7 @@ private function addServiceInstance($id, $definition) $class = $this->dumpValue($class); - if (0 === strpos($class, "'") && !preg_match('/^\'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) { + if (0 === strpos($class, "'") && false === strpos($class, '$') && !preg_match('/^\'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) { throw new InvalidArgumentException(sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id)); } @@ -539,7 +552,7 @@ private function addServiceConfigurator($id, $definition, $variableName = 'insta $class = $this->dumpValue($callable[0]); // If the class is a string we can optimize call_user_func away - if (strpos($class, "'") === 0) { + if (0 === strpos($class, "'") && false === strpos($class, '$')) { return sprintf(" %s::%s(\$%s);\n", $this->dumpLiteralClass($class), $callable[1], $variableName); } @@ -572,6 +585,7 @@ private function addService($id, $definition) if ($definition->isSynthetic()) { $return[] = '@throws RuntimeException always since this service is expected to be injected dynamically'; } elseif ($class = $definition->getClass()) { + $class = $this->container->resolveEnvPlaceholders($class); $return[] = sprintf('@return %s A %s instance', 0 === strpos($class, '%') ? 'object' : '\\'.ltrim($class, '\\'), ltrim($class, '\\')); } elseif ($definition->getFactory()) { $factory = $definition->getFactory(); @@ -595,6 +609,7 @@ private function addService($id, $definition) } $return = str_replace("\n * \n", "\n *\n", implode("\n * ", $return)); + $return = $this->container->resolveEnvPlaceholders($return); $doc = ''; if ($definition->isShared()) { @@ -654,7 +669,7 @@ private function addService($id, $definition) $code .= sprintf(" throw new RuntimeException('You have requested a synthetic service (\"%s\"). The DIC does not know how to construct this service.');\n }\n", $id); } else { if ($definition->isDeprecated()) { - $code .= sprintf(" @trigger_error(%s, E_USER_DEPRECATED);\n\n", var_export($definition->getDeprecationMessage($id), true)); + $code .= sprintf(" @trigger_error(%s, E_USER_DEPRECATED);\n\n", $this->export($definition->getDeprecationMessage($id))); } $code .= @@ -720,7 +735,7 @@ private function addNewInstance(Definition $definition, $return, $instantiation, $class = $this->dumpValue($callable[0]); // If the class is a string we can optimize call_user_func away - if (strpos($class, "'") === 0) { + if (0 === strpos($class, "'") && false === strpos($class, '$')) { if ("''" === $class) { throw new RuntimeException(sprintf('Cannot dump definition: The "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id)); } @@ -735,7 +750,7 @@ private function addNewInstance(Definition $definition, $return, $instantiation, return sprintf(" $return{$instantiation}call_user_func(array(%s, '%s')%s);\n", $this->dumpValue($callable[0]), $callable[1], $arguments ? ', '.implode(', ', $arguments) : ''); } - return sprintf(" $return{$instantiation}\\%s(%s);\n", $callable, $arguments ? implode(', ', $arguments) : ''); + return sprintf(" $return{$instantiation}%s(%s);\n", $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : ''); } if (false !== strpos($class, '$')) { @@ -904,7 +919,7 @@ private function addMethodMap() $code = " \$this->methodMap = array(\n"; ksort($definitions); foreach ($definitions as $id => $definition) { - $code .= ' '.var_export($id, true).' => '.var_export($this->generateMethodName($id), true).",\n"; + $code .= ' '.$this->export($id).' => '.$this->export($this->generateMethodName($id)).",\n"; } return $code." );\n"; @@ -925,7 +940,7 @@ private function addPrivateServices() ksort($definitions); foreach ($definitions as $id => $definition) { if (!$definition->isPublic()) { - $code .= ' '.var_export($id, true)." => true,\n"; + $code .= ' '.$this->export($id)." => true,\n"; } } @@ -962,7 +977,7 @@ private function addAliases() while (isset($aliases[$id])) { $id = (string) $aliases[$id]; } - $code .= ' '.var_export($alias, true).' => '.var_export($id, true).",\n"; + $code .= ' '.$this->export($alias).' => '.$this->export($id).",\n"; } return $code." );\n"; @@ -979,7 +994,23 @@ private function addDefaultParametersMethod() return ''; } - $parameters = $this->exportParameters($this->container->getParameterBag()->all()); + $php = array(); + $dynamicPhp = array(); + + foreach ($this->container->getParameterBag()->all() as $key => $value) { + if ($key !== $resolvedKey = $this->container->resolveEnvPlaceholders($key)) { + throw new InvalidArgumentException(sprintf('Parameter name cannot use env parameters: %s.', $resolvedKey)); + } + $export = $this->exportParameters(array($value)); + $export = explode('0 => ', substr(rtrim($export, " )\n"), 7, -1), 2); + + if (preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $export[1])) { + $dynamicPhp[$key] = sprintf('%scase %s: $value = %s; break;', $export[0], $this->export($key), $export[1]); + } else { + $php[] = sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]); + } + } + $parameters = sprintf("array(\n%s\n%s)", implode("\n", $php), str_repeat(' ', 8)); $code = ''; if ($this->container->isFrozen()) { @@ -992,9 +1023,12 @@ public function getParameter($name) { $name = strtolower($name); - if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { + if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]))) { throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } return $this->parameters[$name]; } @@ -1006,7 +1040,7 @@ public function hasParameter($name) { $name = strtolower($name); - return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]); } /** @@ -1023,7 +1057,11 @@ public function setParameter($name, $value) public function getParameterBag() { if (null === $this->parameterBag) { - $this->parameterBag = new FrozenParameterBag($this->parameters); + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); } return $this->parameterBag; @@ -1033,6 +1071,46 @@ public function getParameterBag() if ('' === $this->docStar) { $code = str_replace('/**', '/*', $code); } + + if ($dynamicPhp) { + $loadedDynamicParameters = $this->exportParameters(array_combine(array_keys($dynamicPhp), array_fill(0, count($dynamicPhp), false)), '', 8); + $getDynamicParameter = <<<'EOF' + switch ($name) { +%s + default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%%s" must be defined.', $name)); + } + $this->loadedDynamicParameters[$name] = true; + + return $this->dynamicParameters[$name] = $value; +EOF; + $getDynamicParameter = sprintf($getDynamicParameter, implode("\n", $dynamicPhp)); + } else { + $loadedDynamicParameters = 'array()'; + $getDynamicParameter = str_repeat(' ', 8).'throw new InvalidArgumentException(sprintf(\'The dynamic parameter "%s" must be defined.\', $name));'; + } + + $code .= <<docStar} + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter(\$name) + { +{$getDynamicParameter} + } + +EOF; + } elseif ($dynamicPhp) { + throw new RuntimeException('You cannot dump a not-frozen container with dynamic parameters.'); } $code .= <<export($value); } - $php[] = sprintf('%s%s => %s,', str_repeat(' ', $indent), var_export($key, true), $value); + $php[] = sprintf('%s%s => %s,', str_repeat(' ', $indent), $this->export($key), $value); } return sprintf("array(\n%s\n%s)", implode("\n", $php), str_repeat(' ', $indent - 4)); @@ -1283,7 +1361,7 @@ private function dumpValue($value, $interpolate = true) $factory = $value->getFactory(); if (is_string($factory)) { - return sprintf('\\%s(%s)', $factory, implode(', ', $arguments)); + return sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($factory)), implode(', ', $arguments)); } if (is_array($factory)) { @@ -1358,7 +1436,7 @@ private function dumpValue($value, $interpolate = true) private function dumpLiteralClass($class) { if (false !== strpos($class, '$')) { - throw new RuntimeException('Cannot dump definitions which have a variable class name.'); + return sprintf('${($_ = %s) && false ?: "_"}', $class); } if (0 !== strpos($class, "'") || !preg_match('/^\'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) { throw new RuntimeException(sprintf('Cannot dump definition because of invalid class name (%s)', $class ?: 'n/a')); @@ -1529,9 +1607,9 @@ private function exportTargetDirs() private function export($value) { if (null !== $this->targetDirRegex && is_string($value) && preg_match($this->targetDirRegex, $value, $matches, PREG_OFFSET_CAPTURE)) { - $prefix = $matches[0][1] ? var_export(substr($value, 0, $matches[0][1]), true).'.' : ''; + $prefix = $matches[0][1] ? $this->doExport(substr($value, 0, $matches[0][1])).'.' : ''; $suffix = $matches[0][1] + strlen($matches[0][0]); - $suffix = isset($value[$suffix]) ? '.'.var_export(substr($value, $suffix), true) : ''; + $suffix = isset($value[$suffix]) ? '.'.$this->doExport(substr($value, $suffix)) : ''; $dirname = '__DIR__'; if (0 < $offset = 1 + $this->targetDirMaxMatches - count($matches)) { @@ -1545,6 +1623,23 @@ private function export($value) return $dirname; } - return var_export($value, true); + return $this->doExport($value); + } + + private function doExport($value) + { + $export = var_export($value, true); + + if ("'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$this->getEnv('%s').'")) { + $export = $resolvedExport; + if ("'" === $export[1]) { + $export = substr($export, 3); + } + if (".''" === substr($export, -3)) { + $export = substr($export, 0, -3); + } + } + + return $export; } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index 68ce5005d07aa..f6f2147d822c7 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -55,7 +55,7 @@ public function dump(array $options = array()) $xml = $this->document->saveXML(); $this->document = null; - return $xml; + return $this->container->resolveEnvPlaceholders($xml); } /** diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index 24ec562a403c7..f24650cdcaf76 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -46,7 +46,7 @@ public function dump(array $options = array()) $this->dumper = new YmlDumper(); } - return $this->addParameters()."\n".$this->addServices(); + return $this->container->resolveEnvPlaceholders($this->addParameters()."\n".$this->addServices()); } /** diff --git a/src/Symfony/Component/DependencyInjection/Exception/EnvNotFoundException.php b/src/Symfony/Component/DependencyInjection/Exception/EnvNotFoundException.php new file mode 100644 index 0000000000000..577095e88b493 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/EnvNotFoundException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Exception; + +/** + * This exception is thrown when an environment variable is not found. + * + * @author Nicolas Grekas + */ +class EnvNotFoundException extends InvalidArgumentException +{ + public function __construct($name) + { + parent::__construct(sprintf('Environment variable not found: "%s".', $name)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Exception/EnvParameterException.php b/src/Symfony/Component/DependencyInjection/Exception/EnvParameterException.php new file mode 100644 index 0000000000000..44dbab45b6284 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/EnvParameterException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Exception; + +/** + * This exception wraps exceptions whose messages contain a reference to an env parameter. + * + * @author Nicolas Grekas + */ +class EnvParameterException extends InvalidArgumentException +{ + public function __construct(array $usedEnvs, \Exception $previous = null) + { + parent::__construct(sprintf('Incompatible use of dynamic environment variables "%s" found in parameters.', implode('", "', $usedEnvs)), 0, $previous); + } +} diff --git a/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php b/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php new file mode 100644 index 0000000000000..68a9ddd5b80d5 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\ParameterBag; + +use Symfony\Component\DependencyInjection\Exception\RuntimeException; + +/** + * @author Nicolas Grekas + */ +class EnvPlaceholderParameterBag extends ParameterBag +{ + private $envPlaceholders = array(); + + /** + * {@inheritdoc} + */ + public function get($name) + { + if (0 === strpos($name, 'env(') && ')' === substr($name, -1) && 'env()' !== $name) { + $env = substr($name, 4, -1); + + if (isset($this->envPlaceholders[$env])) { + return $this->envPlaceholders[$env][0]; + } + if (preg_match('/\W/', $env)) { + throw new InvalidArgumentException(sprintf('Invalid %s name: only "word" characters are allowed.', $name)); + } + + if ($this->has($name)) { + $defaultValue = parent::get($name); + + if (!is_scalar($defaultValue)) { + throw new RuntimeException(sprintf('The default value of an env() parameter must be scalar, but "%s" given to "%s".', gettype($defaultValue), $name)); + } + } + + return $this->envPlaceholders[$env][] = sprintf('env_%s_%s', $env, md5($name.uniqid(mt_rand(), true))); + } + + return parent::get($name); + } + + /** + * Returns the map of env vars used in the resolved parameter values to their placeholders. + * + * @return string[][] A map of env var names to their placeholders + */ + public function getEnvPlaceholders() + { + return $this->envPlaceholders; + } + + /** + * Merges the env placeholders of another EnvPlaceholderParameterBag. + */ + public function mergeEnvPlaceholders(self $bag) + { + $this->envPlaceholders = array_merge_recursive($this->envPlaceholders, $bag->getEnvPlaceholders()); + } +} diff --git a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php index 8f5972ce9acbc..8c8806290f87a 100644 --- a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php +++ b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php @@ -189,13 +189,14 @@ public function resolveString($value, array $resolving = array()) // as the preg_replace_callback throw an exception when trying // a non-string in a parameter value if (preg_match('/^%([^%\s]+)%$/', $value, $match)) { - $key = strtolower($match[1]); + $key = $match[1]; + $lcKey = strtolower($key); - if (isset($resolving[$key])) { + if (isset($resolving[$lcKey])) { throw new ParameterCircularReferenceException(array_keys($resolving)); } - $resolving[$key] = true; + $resolving[$lcKey] = true; return $this->resolved ? $this->get($key) : $this->resolveValue($this->get($key), $resolving); } @@ -206,8 +207,9 @@ public function resolveString($value, array $resolving = array()) return '%%'; } - $key = strtolower($match[1]); - if (isset($resolving[$key])) { + $key = $match[1]; + $lcKey = strtolower($key); + if (isset($resolving[$lcKey])) { throw new ParameterCircularReferenceException(array_keys($resolving)); } @@ -218,7 +220,7 @@ public function resolveString($value, array $resolving = array()) } $resolved = (string) $resolved; - $resolving[$key] = true; + $resolving[$lcKey] = true; return $this->isResolved() ? $resolved : $this->resolveString($resolved, $resolving); }, $value); diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index eb7890b506f6b..b5c396da2b637 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -26,6 +26,7 @@ use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\ExpressionLanguage\Expression; @@ -505,6 +506,14 @@ public function testMerge() $config->setDefinition('foo', new Definition('BazClass')); $container->merge($config); $this->assertEquals('BazClass', $container->getDefinition('foo')->getClass(), '->merge() overrides already defined services'); + + $container = new ContainerBuilder(); + $bag = new EnvPlaceholderParameterBag(); + $bag->get('env(Foo)'); + $config = new ContainerBuilder($bag); + $config->resolveEnvPlaceholders($bag->get('env(Bar)')); + $container->merge($config); + $this->assertEquals(array('Foo' => 0, 'Bar' => 1), $container->getEnvCounters()); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index d155a9ac763e2..7f93f9f9834f2 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -11,11 +11,13 @@ namespace Symfony\Component\DependencyInjection\Tests\Dumper; +use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Variable; use Symfony\Component\ExpressionLanguage\Expression; @@ -285,6 +287,30 @@ public function testDumpAutowireData() $this->assertEquals(file_get_contents(self::$fixturesPath.'/php/services24.php'), $dumper->dump()); } + public function testEnvParameter() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services26.yml'); + $container->compile(); + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services26.php', $dumper->dump(), '->dump() dumps inline definitions which reference service_container'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\EnvParameterException + * @expectedExceptionMessage Incompatible use of dynamic environment variables "FOO" found in parameters. + */ + public function testUnusedEnvParameter() + { + $container = new ContainerBuilder(); + $container->getParameter('env(FOO)'); + $container->compile(); + $dumper = new PhpDumper($container); + $dumper->dump(); + } + public function testInlinedDefinitionReferencingServiceContainer() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services10.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services10.php index e573052e1c5d7..c300e5a4c49f0 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services10.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services10.php @@ -69,9 +69,12 @@ public function getParameter($name) { $name = strtolower($name); - if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { + if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]))) { throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } return $this->parameters[$name]; } @@ -83,7 +86,7 @@ public function hasParameter($name) { $name = strtolower($name); - return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]); } /** @@ -100,12 +103,33 @@ public function setParameter($name, $value) public function getParameterBag() { if (null === $this->parameterBag) { - $this->parameterBag = new FrozenParameterBag($this->parameters); + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); } return $this->parameterBag; } + private $loadedDynamicParameters = array(); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + /** * Gets the default parameters. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services12.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services12.php index d5059b6f4b750..5c299f55d69c1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services12.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services12.php @@ -73,9 +73,12 @@ public function getParameter($name) { $name = strtolower($name); - if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { + if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]))) { throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } return $this->parameters[$name]; } @@ -87,7 +90,7 @@ public function hasParameter($name) { $name = strtolower($name); - return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]); } /** @@ -104,12 +107,43 @@ public function setParameter($name, $value) public function getParameterBag() { if (null === $this->parameterBag) { - $this->parameterBag = new FrozenParameterBag($this->parameters); + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); } return $this->parameterBag; } + private $loadedDynamicParameters = array( + 'foo' => false, + 'buz' => false, + ); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + switch ($name) { + case 'foo': $value = ('wiz'.$this->targetDirs[1]); break; + case 'buz': $value = $this->targetDirs[2]; break; + default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + $this->loadedDynamicParameters[$name] = true; + + return $this->dynamicParameters[$name] = $value; + } + /** * Gets the default parameters. * @@ -118,10 +152,8 @@ public function getParameterBag() protected function getDefaultParameters() { return array( - 'foo' => ('wiz'.$this->targetDirs[1]), 'bar' => __DIR__, 'baz' => (__DIR__.'/PhpDumperTest.php'), - 'buz' => $this->targetDirs[2], ); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services26.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services26.php new file mode 100644 index 0000000000000..d834ae2865dae --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services26.php @@ -0,0 +1,154 @@ +parameters = $this->getDefaultParameters(); + + $this->services = array(); + $this->methodMap = array( + 'test' => 'getTestService', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped frozen container.'); + } + + /** + * {@inheritdoc} + */ + public function isFrozen() + { + return true; + } + + /** + * Gets the 'test' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return object A %env(FOO)% instance + */ + protected function getTestService() + { + $class = $this->getEnv('FOO'); + + return $this->services['test'] = new $class($this->getEnv('Bar'), 'foo'.$this->getEnv('FOO').'baz'); + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) + { + $name = strtolower($name); + + if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + + return $this->parameters[$name]; + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) + { + $name = strtolower($name); + + return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) + { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + /** + * {@inheritdoc} + */ + public function getParameterBag() + { + if (null === $this->parameterBag) { + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); + } + + return $this->parameterBag; + } + + private $loadedDynamicParameters = array( + 'bar' => false, + ); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + switch ($name) { + case 'bar': $value = $this->getEnv('FOO'); break; + default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + $this->loadedDynamicParameters[$name] = true; + + return $this->dynamicParameters[$name] = $value; + } + + /** + * Gets the default parameters. + * + * @return array An array of the default parameters + */ + protected function getDefaultParameters() + { + return array( + 'env(foo)' => 'foo', + ); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php index c62fedaa3eccd..2ccadfe16d384 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php @@ -357,9 +357,12 @@ public function getParameter($name) { $name = strtolower($name); - if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { + if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]))) { throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } return $this->parameters[$name]; } @@ -371,7 +374,7 @@ public function hasParameter($name) { $name = strtolower($name); - return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]); } /** @@ -388,12 +391,33 @@ public function setParameter($name, $value) public function getParameterBag() { if (null === $this->parameterBag) { - $this->parameterBag = new FrozenParameterBag($this->parameters); + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); } return $this->parameterBag; } + private $loadedDynamicParameters = array(); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + /** * Gets the default parameters. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services26.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services26.yml new file mode 100644 index 0000000000000..2ef23c1af545f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services26.yml @@ -0,0 +1,10 @@ +parameters: + env(FOO): foo + bar: '%env(FOO)%' + +services: + test: + class: '%env(FOO)%' + arguments: + - '%env(Bar)%' + - 'foo%bar%baz' diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 125da81d5ca4d..f1013f23c61b2 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -16,7 +16,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Loader\IniFileLoader; @@ -611,7 +610,8 @@ protected function prepareContainer(ContainerBuilder $container) */ protected function getContainerBuilder() { - $container = new ContainerBuilder(new ParameterBag($this->getKernelParameters())); + $container = new ContainerBuilder(); + $container->getParameterBag()->add($this->getKernelParameters()); if (class_exists('ProxyManager\Configuration') && class_exists('Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator')) { $container->setProxyInstantiator(new RuntimeInstantiator()); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy