8000 [DI] Allow injecting ENV parameters at runtime using %env(MY_ENV_VAR)… · symfony/symfony@f7e0a54 · GitHub
[go: up one dir, main page]

Skip to content
Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f7e0a54

Browse files
[DI] Allow injecting ENV parameters at runtime using %env(MY_ENV_VAR)% syntax
1 parent ab93dd8 commit f7e0a54

File tree

10 files changed

+335
-19
lines changed

10 files changed

+335
-19
lines changed

src/Symfony/Component/DependencyInjection/Container.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111

1212
namespace Symfony\Component\DependencyInjection;
1313

14+
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
1415
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1516
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
1617
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
1718
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
18-
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
19+
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
1920
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
2021

2122
/**
@@ -70,13 +71,14 @@ class Container implements ResettableContainerInterface
7071
protected $loading = array();
7172

7273
private $underscoreMap = array('_' => '', '.' => '_', '\\' => '_');
74+
private $envCache = array();
7375

7476
/**
7577
* @param ParameterBagInterface $parameterBag A ParameterBagInterface instance
7678
*/
7779
public function __construct(ParameterBagInterface $parameterBag = null)
7880
{
79-
$this->parameterBag = $parameterBag ?: new ParameterBag();
81+
$this->parameterBag = $parameterBag ?: new EnvPlaceholderParameterBag();
8082
}
8183

8284
/**
@@ -372,6 +374,33 @@ public static function underscore($id)
372374
return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), str_replace('_', '.', $id)));
373375
}
374376

377+
/**
378+
* Fetches a variable from the environment.
379+
*
380+
* @param string The name of the environment variable
381+
*
382+
* @return scalar The value to use for the provided environment variable name
383+
*
384+
* @throws EnvNotFoundException When the environment variable is not found and has no default value
385+
*/
386+
protected function getEnv($name)
387+
{
388+
if (isset($this->envCache[$name]) || array_key_exists($name, $this->envCache)) {
389+
return $this->envCache[$name];
390+
}
391+
if (isset($_ENV[$name])) {
392+
return $this->envCache[$name] = $_ENV[$name];
393+
}
394+
if (false !== $env = getenv($name)) {
395+
return $this->envCache[$name] = $env;
396+
}
397+
if (!$this->hasParameter("env($name)")) {
398+
throw new EnvNotFoundException($name);
399+
}
400+
401+
return $this->envCache[$name] = $this->getParameter("env($name)");
402+
}
403+
375404
private function __clone()
376405
{
377406
}

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
2222
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
2323
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
24+
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
2425
use Symfony\Component\Config\Resource\FileResource;
2526
use Symfony\Component\Config\Resource\ResourceInterface;
2627
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
@@ -89,6 +90,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
8990
*/
9091
private $usedTags = array();
9192

93+
/**
94+
* @var string[] A map of env var names to their placeholders
95+
*/
96+
private $envPlaceholders = array();
97+
9298
private $compiled = false;
9399

94100
/**
@@ -551,8 +557,11 @@ public function compile()
551557
}
552558

553559
$this->extensionConfigs = array();
560+
$bag = $this->getParameterBag();
554561

555562
parent::compile();
563+
564+
$this->envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : array();
556565
}
557566

558567
/**
@@ -995,6 +1004,18 @@ public function getExpressionLanguageProviders()
9951004
return $this->expressionLanguageProviders;
9961005
}
9971006

1007+
/**
1008+
* Returns the map of env vars used in the resolved parameter values to their placeholders.
1009+
*
1010+
* @return string[] A map of env var names to their placeholders
1011+
*/
1012+
public function getEnvPlaceholders()
1013+
{
1014+
$bag = $this->getParameterBag();
1015+
1016+
return $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : $this->envPlaceholders;
1017+
}
1018+
9981019
/**
9991020
* Returns the Service Conditionals.
10001021
*

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class PhpDumper extends Dumper
6060
private $docStar;
6161
private $serviceIdToMethodNameMap;
6262
private $usedMethodNames;
63+
private $envPlaceholders;
6364

6465
/**
6566
* @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface
@@ -74,6 +75,7 @@ public function __construct(ContainerBuilder $container)
7475
parent::__construct($container);
7576

7677
$this->inlinedDefinitions = new \SplObjectStorage();
78+
$this->envPlaceholders = $container->getEnvPlaceholders();
7779
}
7880

7981
/**
@@ -384,7 +386,7 @@ private function addServiceInstance($id, $definition)
384386

385387
$class = $this->dumpValue($class);
386388

387-
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)) {
389+
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)) {
388390
throw new InvalidArgumentException(sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id));
389391
}
390392

@@ -539,7 +541,7 @@ private function addServiceConfigurator($id, $definition, $variableName = 'insta
539541

540542
$class = $this->dumpValue($callable[0]);
541543
// If the class is a string we can optimize call_user_func away
542-
if (strpos($class, "'") === 0) {
544+
if (0 === strpos($class, "'") && false === strpos($class, '$')) {
543545
return sprintf(" %s::%s(\$%s);\n", $this->dumpLiteralClass($class), $callable[1], $variableName);
544546
}
545547

@@ -572,6 +574,7 @@ private function addService($id, $definition)
572574
if ($definition->isSynthetic()) {
573575
$return[] = '@throws RuntimeException always since this service is expected to be injected dynamically';
574576
} elseif ($class = $definition->getClass()) {
577+
$class = $this->reverseEnvPlaceholders($class);
575578
$return[] = sprintf('@return %s A %s instance', 0 === strpos($class, '%') ? 'object' : '\\'.ltrim($class, '\\'), ltrim($class, '\\'));
576579
} elseif ($definition->getFactory()) {
577580
$factory = $definition->getFactory();
@@ -595,6 +598,7 @@ private function addService($id, $definition)
595598
}
596599

597600
$return = str_replace("\n * \n", "\n *\n", implode("\n * ", $return));
601+
$return = $this->reverseEnvPlaceholders($return);
598602

599603
$doc = '';
600604
if ($definition->isShared()) {
@@ -654,7 +658,7 @@ private function addService($id, $definition)
654658
$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);
655659
} else {
656660
if ($definition->isDeprecated()) {
657-
$code .= sprintf(" @trigger_error(%s, E_USER_DEPRECATED);\n\n", var_export($definition->getDeprecationMessage($id), true));
661+
$code .= sprintf(" @trigger_error(%s, E_USER_DEPRECATED);\n\n", $this->export($definition->getDeprecationMessage($id)));
658662
}
659663

660664
$code .=
@@ -720,7 +724,7 @@ private function addNewInstance(Definition $definition, $return, $instantiation,
720724

721725
$class = $this->dumpValue($callable[0]);
722726
// If the class is a string we can optimize call_user_func away
723-
if (strpos($class, "'") === 0) {
727+
if (0 === strpos($class, "'") && false === strpos($class, '$')) {
724728
if ("''" === $class) {
725729
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));
726730
}
@@ -735,7 +739,7 @@ private function addNewInstance(Definition $definition, $return, $instantiation,
735739
return sprintf(" $return{$instantiation}call_user_func(array(%s, '%s')%s);\n", $this->dumpValue($callable[0]), $callable[1], $arguments ? ', '.implode(', ', $arguments) : '');
736740
}
737741

738-
return sprintf(" $return{$instantiation}\\%s(%s);\n", $callable, $arguments ? implode(', ', $arguments) : '');
742+
return sprintf(" $return{$instantiation}%s(%s);\n", $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '');
739743
}
740744

741745
if (false !== strpos($class, '$')) {
@@ -904,7 +908,7 @@ private function addMethodMap()
904908
$code = " \$this->methodMap = array(\n";
905909
ksort($definitions);
906910
foreach ($definitions as $id => $definition) {
907-
$code .= ' '.var_export($id, true).' => '.var_export($this->generateMethodName($id), true).",\n";
911+
$code .= ' '.$this->export($id).' => '.$this->export($this->generateMethodName($id)).",\n";
908912
}
909913

910914
return $code." );\n";
@@ -925,7 +929,7 @@ private function addPrivateServices()
925929
ksort($definitions);
926930
foreach ($definitions as $id => $definition) {
927931
if (!$definition->isPublic()) {
928-
$code .= ' '.var_export($id, true)." => true,\n";
932+
$code .= ' '.$this->export($id)." => true,\n";
929933
}
930934
}
931935

@@ -962,7 +966,7 @@ private function addAliases()
962966
while (isset($aliases[$id])) {
963967
$id = (string) $aliases[$id];
964968
}
965-
$code .= ' '.var_export($alias, true).' => '.var_export($id, true).",\n";
969+
$code .= ' '.$this->export($alias).' => '.$this->export($id).",\n";
966970
}
967971

968972
return $code." );\n";
@@ -1081,7 +1085,7 @@ private function exportParameters($parameters, $path = '', $indent = 12)
10811085
$value = $this->export($value);
10821086
}
10831087

1084-
$php[] = sprintf('%s%s => %s,', str_repeat(' ', $indent), var_export($key, true), $value);
1088+
$php[] = sprintf('%s%s => %s,', str_repeat(' ', $indent), $this->export($key), $value);
10851089
}
10861090

10871091
return sprintf("array(\n%s\n%s)", implode("\n", $php), str_repeat(' ', $indent - 4));
@@ -1283,7 +1287,7 @@ private function dumpValue($value, $interpolate = true)
12831287
$factory = $value->getFactory();
12841288

12851289
if (is_string($factory)) {
1286-
return sprintf('\\%s(%s)', $factory, implode(', ', $arguments));
1290+
return sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($factory)), implode(', ', $arguments));
12871291
}
12881292

12891293
if (is_array($factory)) {
@@ -1358,7 +1362,7 @@ private function dumpValue($value, $interpolate = true)
13581362
private function dumpLiteralClass($class)
13591363
{
13601364
if (false !== strpos($class, '$')) {
1361-
throw new RuntimeException('Cannot dump definitions which have a variable class name.');
1365+
return sprintf('${($_ = %s) && false ?: "_"}', $class);
13621366
}
13631367
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)) {
13641368
throw new RuntimeException(sprintf('Cannot dump definition because of invalid class name (%s)', $class ?: 'n/a'));
@@ -1529,9 +1533,9 @@ private function exportTargetDirs()
15291533
private function export($value)
15301534
{
15311535
if (null !== $this->targetDirRegex && is_string($value) && preg_match($this->targetDirRegex, $value, $matches, PREG_OFFSET_CAPTURE)) {
1532-
$prefix = $matches[0][1] ? var_export(substr($value, 0, $matches[0][1]), true).'.' : '';
1536+
$prefix = $matches[0][1] ? $this->doExport(substr($value, 0, $matches[0][1])).'.' : '';
15331537
$suffix = $matches[0][1] + strlen($matches[0][0]);
1534-
$suffix = isset($value[$suffix]) ? '.'.var_export(substr($value, $suffix), true) : '';
1538+
$suffix = isset($value[$suffix]) ? '.'.$this->doExport(substr($value, $suffix)) : '';
15351539
$dirname = '__DIR__';
15361540

15371541
if (0 < $offset = 1 + $this->targetDirMaxMatches - count($matches)) {
@@ -1545,6 +1549,34 @@ private function export($value)
15451549
return $dirname;
15461550
}
15471551

1548-
return var_export($value, true);
1552+
return $this->doExport($value);
1553+
}
1554+
1555+
private function doExport($value)
1556+
{
1557+
$export = var_export($value, true);
1558+
1559+
if ("'" === $export[0] && $this->envPlaceholders && false !== strpos($export, 'env_')) {
1560+
foreach ($this->envPlaceholders as $env => $placeholder) {
1561+
$export = str_replace($placeholder, "'.\$this->getEnv('$env').'", $export);
1562+
}
1563+
if ("'" === $export[1]) {
1564+
$export = substr($export, 3);
1565+
}
1566+
if (".''" === substr($export, -3)) {
1567+
$export = substr($export, 0, -3);
1568+
}
1569+
}
1570+
1571+
return $export;
1572+
}
1573+
1574+
private function reverseEnvPlaceholders($string)
1575+
{
1576+
foreach ($this->envPlaceholders as $env => $placeholder) {
1577+
$string = str_replace($placeholder, "%env($env)%", $string);
1578+
}
1579+
1580+
return $string;
15491581
}
15501582
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Exception;
13+
14+
/**
15+
* This exception is thrown when an environment variable is not found.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
class EnvNotFoundException extends InvalidArgumentException
20+
{
21+
public function __construct($name)
22+
{
23+
parent::__construct(sprintf('Environment variable not found: "%s".', $name));
24+
}
25+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\ParameterBag;
13+
14+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
15+
16+
/**
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
class EnvPlaceholderParameterBag extends ParameterBag
20+
{
21+
private $envPlaceholders = array();
22+
23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function get($name)
27+
{
28+
if (0 === strpos($name, 'env(') && ')' === substr($name, -1) && 'env()' !== $name) {
29+
$env = substr($name, 4, -1);
30+
31+
if (isset($this->envPlaceholders[$env])) {
32+
return $this->envPlaceholders[$env];
33+
}
34+
if (!preg_match('/^[A-Z_]+$/i', $env)) {
35+
throw new InvalidArgumentException(sprintf('Invalid %s name: only characters "A-Z" and "_" are allowed.', $name));
36+
}
37+
38+
if ($this->has($name)) {
39+
$defaultValue = parent::get($name);
40+
41+
if (!is_scalar($defaultValue)) {
42+
throw new RuntimeException(sprintf('The default value of an env() parameter must be string, but "%s" given to "%s".', gettype($defaultValue), $name));
43+
}
44+
}
45+
46+
return $this->envPlaceholders[$env] = sprintf('env_%s_%s', $env, md5($name.uniqid(mt_rand(), true)));
47+
}
48+
49+
return parent::get($name);
50+
}
51+
52+
/**
53+
* Returns the map of env vars used in the resolved parameter values to their placeholders.
54+
*
55+
* @return string[] A map of env var names to their placeholders
56+
*/
57+
public function getEnvPlaceholders()
58+
{
59+
return $this->envPlaceholders;
60+
}
61+
}

src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,9 @@ public function resolveString($value, array $resolving = array())
196196
}
197197

198198
$resolving[$key] = true;
199+
$value = $this->get($match[1]);
199200

200-
return $this->resolved ? $this->get($key) : $this->resolveValue($this->get($key), $resolving);
201+
return $this->resolved ? $value : $this->resolveValue($value, $resolving);
201202
}
202203

203204
return preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($resolving, $value) {

0 commit comments

Comments
 (0)
0