diff --git a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php index 5d83a6a8f5..cc5d9adc79 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 @@ class Compiler */ 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 9dd0512aef..7fbfff2291 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 @@ class Container implements ResettableContainerInterface 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 997d06f41b..b82465480d 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; 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 @@ class ContainerBuilder extends Container implements TaggedContainerInterface $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 @@ class ContainerBuilder extends Container implements TaggedContainerInterface } $this->extensionConfigs = array(); + $bag = $this->getParameterBag(); parent::compile(); + + $this->envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : array(); } /** @@ -995,6 +1021,56 @@ class ContainerBuilder extends Container implements TaggedContainerInterface 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 c569232f20..09c111a9c6 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php @@ -81,7 +81,7 @@ class GraphvizDumper extends Dumper } } - 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 d2d567093c..b3a4008369 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\Container; 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 @@ class PhpDumper extends Dumper * @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 @@ class PhpDumper extends Dumper ; $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 @@ class PhpDumper extends Dumper $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 @@ class PhpDumper extends Dumper $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 @@ class PhpDumper extends Dumper 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 @@ class PhpDumper extends Dumper } $return = str_replace("\n * \n", "\n *\n", implode("\n * ", $return)); + $return = $this->container->resolveEnvPlaceholders($return); $doc = ''; if ($definition->isShared()) { @@ -654,7 +669,7 @@ EOF; $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 @@ EOF; $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 @@ EOF; 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 @@ EOF; $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 @@ EOF; 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 @@ EOF; 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 @@ EOF; 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 @@ EOF; { $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 @@ EOF; { $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 @@ EOF; 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 @@ EOF; 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 @@ EOF; $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 @@ EOF; 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 @@ EOF; 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 @@ EOF; 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 68ce5005d0..f6f2147d82 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -55,7 +55,7 @@ class XmlDumper extends Dumper $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 24ec562a40..f24650cdca 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -46,7 +46,7 @@ class YamlDumper extends Dumper $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 0000000000..577095e88b --- /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 0000000000..44dbab45b6 --- /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 0000000000..68a9ddd5b8 --- /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 8f5972ce9a..8c8806290f 100644 --- a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php +++ b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php @@ -189,13 +189,14 @@ class ParameterBag implements ParameterBagInterface // 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 @@ class ParameterBag implements ParameterBagInterface 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 @@ class ParameterBag implements ParameterBagInterface } $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 eb7890b506..b5c396da2b 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\Exception\ServiceNotFoundException; 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 @@ class ContainerBuilderTest extends \PHPUnit_Framework_TestCase $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 d155a9ac76..7f93f9f983 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 @@ class PhpDumperTest extends \PHPUnit_Framework_TestCase $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 e573052e1c..c300e5a4c4 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 @@ class ProjectServiceContainer extends Container { $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 @@ class ProjectServiceContainer extends Container { $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 @@ class ProjectServiceContainer extends Container 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 d5059b6f4b..5c299f55d6 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 @@ class ProjectServiceContainer extends Container { $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 @@ class ProjectServiceContainer extends Container { $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 @@ class ProjectServiceContainer extends Container 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 @@ class ProjectServiceContainer extends Container 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 0000000000..d834ae2865 --- /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 c62fedaa3e..2ccadfe16d 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 @@ class ProjectServiceContainer extends Container { $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 @@ class ProjectServiceContainer extends Container { $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 @@ class ProjectServiceContainer extends Container 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 0000000000..2ef23c1af5 --- /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 125da81d5c..f1013f23c6 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -16,7 +16,6 @@ use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; 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 @@ abstract class Kernel implements KernelInterface, TerminableInterface */ 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());