Skip to content

Commit 8bc1103

Browse files
[DI] Allow processing env vars
1 parent 6dcc0cb commit 8bc1103

17 files changed

+974
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
3.4.0
55
-----
66

7+
* added `EnvVarProcessorInterface` and corresponding "container.env_var_processor" tag for processing env vars
78
* added support for ignore-on-uninitialized references
89
* deprecated service auto-registration while autowiring
910
* deprecated the ability to check for the initialization of a private service with the `Container::initialized()` method

Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public function __construct()
4343
100 => array(
4444
$resolveClassPass = new ResolveClassPass(),
4545
new ResolveInstanceofConditionalsPass(),
46+
new RegisterEnvVarProcessorsPass(),
4647
),
4748
);
4849

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
18+
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
19+
use Symfony\Component\DependencyInjection\ServiceLocator;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
22+
/**
23+
* Creates the container.env_var_processors_locator service.
24+
*
25+
* @author Nicolas Grekas <[email protected]>
26+
*/
27+
class RegisterEnvVarProcessorsPass implements CompilerPassInterface
28+
{
29+
private static $allowedTypes = array('array', 'bool', 'float', 'int', 'string');
30+
31+
public function process(ContainerBuilder $container)
32+
{
33+
$bag = $container->getParameterBag();
34+
$types = array();
35+
$processors = array();
36+
foreach ($container->findTaggedServiceIds('container.env_var_processor') as $id => $tags) {
37+
foreach ($tags as $attr) {
38+
if (!$r = $container->getReflectionClass($class = $container->getDefinition($id)->getClass())) {
39+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
40+
} elseif (!$r->isSubclassOf(EnvVarProcessorInterface::class)) {
41+
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EnvVarProcessorInterface::class));
42+
}
43+
foreach ($class::getProvidedTypes() as $prefix => $type) {
44+
$processors[$prefix] = new ServiceClosureArgument(new Reference($id));
45+
$types[$prefix] = self::validateProvidedTypes($type, $class);
46+
}
47+
}
48+
}
49+
50+
if ($processors) {
51+
if ($bag instanceof EnvPlaceholderParameterBag) {
52+
$bag->setProvidedTypes($types);
53+
}
54+
$container->register('container.env_var_processors_locator', ServiceLocator::class)
55+
->setArguments(array($processors))
56+
;
57+
}
58+
}
59+
60+
private static function validateProvidedTypes($types, $class)
61+
{
62+
$types = explode('|', $types);
63+
64+
foreach ($types as $type) {
65+
if (!in_array($type, self::$allowedTypes)) {
66+
throw new InvalidArgumentException(sprintf('Invalid type "%s" returned by "%s::getProvidedTypes()", expected one of "%s".', $type, $class, implode('", "', self::$allowedTypes)));
67+
}
68+
}
69+
70+
return $types;
71+
}
72+
}

Container.php

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
1515
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
1617
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
1718
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
1819
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
@@ -48,6 +49,7 @@ class Container implements ResettableContainerInterface
4849
protected $methodMap = array();
4950
protected $aliases = array();
5051
protected $loading = array();
52+
protected $resolving = array();
5153

5254
/**
5355
* @internal
@@ -62,6 +64,7 @@ class Container implements ResettableContainerInterface
6264
private $underscoreMap = array('_' => '', '.' => '_', '\\' => '_');
6365
private $envCache = array();
6466
private $compiled = false;
67+
private $getEnv;
6568

6669
/**
6770
* @param ParameterBagInterface $parameterBag A ParameterBagInterface instance
@@ -438,23 +441,37 @@ protected function load($file)
438441
*/
439442
protected function getEnv($name)
440443
{
444+
if (isset($this->resolving[$envName = "env($name)"])) {
445+
throw new ParameterCircularReferenceException(array_keys($this->resolving));
446+
}
441447
if (isset($this->envCache[$name]) || array_key_exists($name, $this->envCache)) {
442448
return $this->envCache[$name];
443449
}
444-
if (isset($_SERVER[$name]) && 0 !== strpos($name, 'HTTP_')) {
445-
return $this->envCache[$name] = $_SERVER[$name];
446-
}
447-
if (isset($_ENV[$name])) {
448-
return $this->envCache[$name] = $_ENV[$name];
450+
if (!$this->has($id = 'container.env_var_processors_locator')) {
451+
$this->set($id, new ServiceLocator(array()));
449452
}
450-
if (false !== ($env = getenv($name)) && null !== $env) { // null is a possible value because of thread safety issues
451-
return $this->envCache[$name] = $env;
453+
if (!$this->getEnv) {
454+
$this->getEnv = new \ReflectionMethod($this, __FUNCTION__);
455+
$this->getEnv->setAccessible(true);
456+
$this->getEnv = $this->getEnv->getClosure($this);
452457
}
453-
if (!$this->hasParameter("env($name)")) {
454-
throw new EnvNotFoundException($name);
458+
$processors = $this->get($id);
459+
460+
if (false !== $i = strpos($name, ':')) {
461+
$prefix = substr($name, 0, $i);
462+
$localName = substr($name, 1 + $i);
463+
} else {
464+
$prefix = 'string';
465+
$localName = $name;
455466
}
467+
$processor = $processors->has($prefix) ? $processors->get($prefix) : new EnvVarProcessor($this);
456468

457-
return $this->envCache[$name] = $this->getParameter("env($name)");
469+
$this->resolving[$envName] = true;
470+
try {
471+
return $this->envCache[$name] = $processor->getEnv($prefix, $localName, $this->getEnv);
472+
} finally {
473+
unset($this->resolving[$envName]);
474+
}
458475
}
459476

460477
/**

ContainerBuilder.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,20 +1472,26 @@ public static function hash($value)
14721472
protected function getEnv($name)
14731473
{
14741474
$value = parent::getEnv($name);
1475+
$bag = $this->getParameterBag();
14751476

1476-
if (!is_string($value) || !$this->getParameterBag() instanceof EnvPlaceholderParameterBag) {
1477+
if (!is_string($value) || !$bag instanceof EnvPlaceholderParameterBag) {
14771478
return $value;
14781479
}
14791480

1480-
foreach ($this->getParameterBag()->getEnvPlaceholders() as $env => $placeholders) {
1481+
foreach ($bag->getEnvPlaceholders() as $env => $placeholders) {
14811482
if (isset($placeholders[$value])) {
1482-
$bag = new ParameterBag($this->getParameterBag()->all());
1483+
$bag = new ParameterBag($bag->all());
14831484

14841485
return $bag->unescapeValue($bag->get("env($name)"));
14851486
}
14861487
}
14871488

1488-
return $value;
1489+
$this->resolving["env($name)"] = true;
1490+
try {
1491+
return $bag->unescapeValue($this->resolveEnvPlaceholders($bag->escapeValue($value), true));
1492+
} finally {
1493+
unset($this->resolving["env($name)"]);
1494+
}
14891495
}
14901496

14911497
/**

Dumper/PhpDumper.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,7 +1112,7 @@ private function addDefaultParametersMethod()
11121112
$export = $this->exportParameters(array($value));
11131113
$export = explode('0 => ', substr(rtrim($export, " )\n"), 7, -1), 2);
11141114

1115-
if (preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $export[1])) {
1115+
if (preg_match("/\\\$this->(?:getEnv\('(?:\w++:)*+\w++'\)|targetDirs\[\d++\])/", $export[1])) {
11161116
$dynamicPhp[$key] = sprintf('%scase %s: $value = %s; break;', $export[0], $this->export($key), $export[1]);
11171117
} else {
11181118
$php[] = sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
@@ -1685,7 +1685,7 @@ private function dumpParameter($name)
16851685
return $dumpedValue;
16861686
}
16871687

1688-
if (!preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $dumpedValue)) {
1688+
if (!preg_match("/\\\$this->(?:getEnv\('(?:\w++:)*+\w++'\)|targetDirs\[\d++\])/", $dumpedValue)) {
16891689
return sprintf("\$this->parameters['%s']", $name);
16901690
}
16911691
}
@@ -1880,13 +1880,16 @@ private function doExport($value)
18801880
{
18811881
$export = var_export($value, true);
18821882

1883-
if ("'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$this->getEnv('%s').'")) {
1883+
if ("'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$this->getEnv('string:%s').'")) {
18841884
$export = $resolvedExport;
1885-
if ("'" === $export[1]) {
1886-
$export = substr($export, 3);
1887-
}
18881885
if (".''" === substr($export, -3)) {
18891886
$export = substr($export, 0, -3);
1887+
if ("'" === $export[1]) {
1888+
$export = substr_replace($export, '', 18, 7);
1889+
}
1890+
}
1891+
if ("'" === $export[1]) {
1892+
$export = substr($export, 3);
18901893
}
18911894
}
18921895

EnvVarProcessor.php

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection;
13+
14+
use Symfony\Component\Config\Util\XmlUtils;
15+
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
16+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
17+
18+
class EnvVarProcessor implements EnvVarProcessorInterface
19+
{
20+
private $container;
21+
22+
public function __construct(ContainerInterface $container)
23+
{
24+
$this->container = $container;
25+
}
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public static function getProvidedTypes()
31+
{
32+
return array(
33+
'base64' => 'string',
34+
'bool' => 'bool',
35+
'const' => 'bool|int|float|string|array',
36+
'file' => 'string',
37+
'float' => 'float',
38+
'int' => 'int',
39+
'json' => 'array',
40+
'resolve' => 'string',
41+
'string' => 'string',
42+
);
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function getEnv($prefix, $name, \Closure $getEnv)
49+
{
50+
$i = strpos($name, ':');
51+
52+
if ('file' === $prefix) {
53+
if (!is_scalar($file = $getEnv($name))) {
54+
throw new RuntimeException(sprintf('Invalid file name: env var "%s" is non-scalar.', $name));
55+
}
56+
if (!file_exists($file)) {
57+
throw new RuntimeException(sprintf('Env "file:%s" not found: %s does not exist.', $name, $file));
58+
}
59+
60+
return file_get_contents($file);
61+
}
62+
63+
if (false !== $i || 'string' !== $prefix) {
64+
if (null === $env = $getEnv($name)) {
65+
return;
66+
}
67+
} elseif (isset($_SERVER[$name]) && 0 !== strpos($name, 'HTTP_')) {
68+
$env = $_SERVER[$name];
69+
} elseif (isset($_ENV[$name])) {
70+
$env = $_ENV[$name];
71+
} elseif (false === ($env = getenv($name)) || null === $env) { // null is a possible value because of thread safety issues
72+
if (!$this->container->hasParameter("env($name)")) {
73+
throw new EnvNotFoundException($name);
74+
}
75+
76+
if (null === $env = $this->container->getParameter("env($name)")) {
77+
return;
78+
}
79+
}
80+
81+
if (!is_scalar($env)) {
82+
throw new RuntimeException(sprintf('Non-scalar env var "%s" cannot be cast to %s.', $name, $prefix));
83+
}
84+
85+
if ('string' === $prefix) {
86+
return (string) $env;
87+
}
88+
89+
if ('bool' === $prefix) {
90+
return (bool) self::phpize($env);
91+
}
92+
93+
if ('int' === $prefix) {
94+
if (!is_numeric($env = self::phpize($env))) {
95+
throw new RuntimeException(sprintf('Non-numeric env var "%s" cannot be cast to int.', $name));
96+
}
97+
98+
return (int) $env;
99+
}
100+
101+
if ('float' === $prefix) {
102+
if (!is_numeric($env = self::phpize($env))) {
103+
throw new RuntimeException(sprintf('Non-numeric env var "%s" cannot be cast to float.', $name));
104+
}
105+
106+
return (float) $env;
107+
}
108+
109+
if ('const' === $prefix) {
110+
if (!defined($env)) {
111+
throw new RuntimeException(sprintf('Env var "%s" maps to undefined constant "%s".', $name, $env));
112+
}
113+
114+
return constant($name);
115+
}
116+
117+
if ('base64' === $prefix) {
118+
return base64_decode($env);
119+
}
120+
121+
if ('json' === $prefix) {
122+
$env = json_decode($env, true, JSON_BIGINT_AS_STRING);
123+
124+
if (JSON_ERROR_NONE !== json_last_error()) {
125+
throw new RuntimeException(sprintf('Invalid JSON in env var "%s": '.json_last_error_msg(), $name));
126+
}
127+
128+
if (!is_array($env)) {
129+
throw new RuntimeException(sprintf('Invalid JSON env var "%s": array expected, %s given.', $name, gettype($env)));
130+
}
131+
132+
return $env;
133+
}
134+
135+
if ('resolve' === $prefix) {
136+
return preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($name) {
137+
if (!isset($match[1])) {
138+
return '%';
139+
}
140+
$value = $this->container->getParameter($match[1]);
141+
if (!is_scalar($value)) {
142+
throw new RuntimeException(sprintf('Parameter "%s" found when resolving env var "%s" must be scalar, "%s" given.', $match[1], $name, gettype($value)));
143+
}
144+
145+
return $value;
146+
}, $env);
147+
}
148+
149+
throw new RuntimeException(sprintf('Unsupported env var prefix "%s".', $prefix));
150+
}
151+
152+
private static function phpize($value)
153+
{
154+
if (!class_exists(XmlUtils::class)) {
155+
throw new RuntimeException('The Symfony Config component is required to cast env vars to "bool", "int" or "float".');
156+
}
157+
158+
return XmlUtils::phpize($value);
159+
}
160+
}

0 commit comments

Comments
 (0)