feature #19681 [DI] Allow injecting ENV parameters at runtime using %env(MY_ENV_VAR)% (nicolas-grekas)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[DI] Allow injecting ENV parameters at runtime using %env(MY_ENV_VAR)%

| Q             | A
| ------------- | ---
| Branch?       | master
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets |  #10138, #7555, #16403, #18155
| License       | MIT
| Doc PR        | https://github.com/symfony/symfony-docs/pull/6918

This is an alternative approach to #18155 for injecting env vars into container configurations.

With this PR, anywhere parameters are allowed, one can use `%env(ENV_VAR)%` to inject a dynamic env var. Additionally, if one sets a value to such parameters in e.g. the `parameter.yml` file (`env(ENV_VAR): foo`), this value will be used as a default value when the env var is not defined. If no default value is specified, an `EnvVarNotFoundException` will be thrown at runtime.

Unlike previous attempts that also used parameters (#16403), the implementation is compatible with DI extensions: before being dumped, env vars are resolved to uniquely identifiable string placeholders that can get through DI extensions manipulations. When dumped, these unique placeholders are replaced by dynamic calls to a getEnv method..

ping @magnusnordlander @dzuelke @fabpot

Commits
-------

bac2132 [DI] Allow injecting ENV parameters at runtime using %env(MY_ENV_VAR)% syntax
This commit is contained in:
Fabien Potencier 2016-09-15 10:44:52 -07:00
commit 1bcade56e6
19 changed files with 666 additions and 44 deletions

View File

@ -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;
}
}
}

View File

@ -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();
}
/**
@ -373,6 +375,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()
{
}

View File

@ -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;
/**
@ -482,6 +493,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;
}
}
}
/**
@ -552,8 +575,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
}
$this->extensionConfigs = array();
$bag = $this->getParameterBag();
parent::compile();
$this->envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : array();
}
/**
@ -996,6 +1022,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.
*

View File

@ -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__');
}
/**

View File

@ -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 .= <<<EOF
private \$loadedDynamicParameters = {$loadedDynamicParameters};
private \$dynamicParameters = array();
/*{$this->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 .= <<<EOF
@ -1081,7 +1159,7 @@ EOF;
$value = $this->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'));
@ -1534,9 +1612,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)) {
@ -1550,6 +1628,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;
}
}

View File

@ -55,7 +55,7 @@ class XmlDumper extends Dumper
$xml = $this->document->saveXML();
$this->document = null;
return $xml;
return $this->container->resolveEnvPlaceholders($xml);
}
/**

View File

@ -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());
}
/**

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <p@tchwork.com>
*/
class EnvNotFoundException extends InvalidArgumentException
{
public function __construct($name)
{
parent::__construct(sprintf('Environment variable not found: "%s".', $name));
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <p@tchwork.com>
*/
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);
}
}

View File

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <p@tchwork.com>
*/
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());
}
}

View File

@ -205,13 +205,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);
}
@ -222,8 +223,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));
}
@ -234,7 +236,7 @@ class ParameterBag implements ParameterBagInterface
}
$resolved = (string) $resolved;
$resolving[$key] = true;
$resolving[$lcKey] = true;
return $this->isResolved() ? $resolved : $this->resolveString($resolved, $resolving);
}, $value);

View File

@ -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());
}
/**

View File

@ -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();

View File

@ -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.
*

View File

@ -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],
);
}
}

View File

@ -0,0 +1,154 @@
<?php
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* ProjectServiceContainer.
*
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*/
class ProjectServiceContainer extends Container
{
private $parameters;
private $targetDirs = array();
/**
* Constructor.
*/
public function __construct()
{
$this->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',
);
}
}

View File

@ -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.
*

View File

@ -0,0 +1,10 @@
parameters:
env(FOO): foo
bar: '%env(FOO)%'
services:
test:
class: '%env(FOO)%'
arguments:
- '%env(Bar)%'
- 'foo%bar%baz'

View File

@ -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());