Skip to content

Commit

Permalink
feature #19681 [DI] Allow injecting ENV parameters at runtime using %…
Browse files Browse the repository at this point in the history
…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        | symfony/symfony-docs#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
  • Loading branch information
fabpot committed Sep 15, 2016
2 parents 835176c + bac2132 commit 1bcade5
Show file tree
Hide file tree
Showing 19 changed files with 666 additions and 44 deletions.
26 changes: 24 additions & 2 deletions src/Symfony/Component/DependencyInjection/Compiler/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
}
}
33 changes: 31 additions & 2 deletions src/Symfony/Component/DependencyInjection/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

/**
Expand Down Expand Up @@ -373,6 +375,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()
{
}
Expand Down
76 changes: 76 additions & 0 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -482,6 +493,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;
}
}
}

/**
Expand Down Expand Up @@ -552,8 +575,11 @@ public function compile()
}

$this->extensionConfigs = array();
$bag = $this->getParameterBag();

parent::compile();

$this->envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : array();
}

/**
Expand Down Expand Up @@ -996,6 +1022,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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__');
}

/**
Expand Down
Loading

0 comments on commit 1bcade5

Please sign in to comment.