Skip to content

Commit f3144e1

Browse files
[DI] Allow processing env vars - handles int/float/file/array/base64 + "container.env_provider"-tagged services
1 parent 324c03d commit f3144e1

File tree

14 files changed

+654
-12
lines changed

14 files changed

+654
-12
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Symfony\Component\DependencyInjection\ContainerBuilder;
3737
use Symfony\Component\DependencyInjection\ContainerInterface;
3838
use Symfony\Component\DependencyInjection\Definition;
39+
use Symfony\Component\DependencyInjection\EnvProviderInterface;
3940
use Symfony\Component\DependencyInjection\Exception\LogicException;
4041
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
4142
use Symfony\Component\DependencyInjection\Reference;
@@ -281,6 +282,8 @@ public function load(array $configs, ContainerBuilder $container)
281282
->addTag('console.command');
282283
$container->registerForAutoconfiguration(ResourceCheckerInterface::class)
283284
->addTag('config_cache.resource_checker');
285+
$container->registerForAutoconfiguration(EnvProviderInterface::class)
286+
->addTag('container.env_provider');
284287
$container->registerForAutoconfiguration(ServiceSubscriberInterface::class)
285288
->addTag('container.service_subscriber');
286289
$container->registerForAutoconfiguration(ArgumentValueResolverInterface::class)

src/Symfony/Component/DependencyInjection/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 RegisterEnvProvidersPass(),
4647
),
4748
);
4849

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\EnvProviderInterface;
16+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
19+
/**
20+
* Creates the container.env_providers_locator service.
21+
*
22+
* @author Nicolas Grekas <p@tchwork.com>
23+
*/
24+
class RegisterEnvProvidersPass implements CompilerPassInterface
25+
{
26+
public function process(ContainerBuilder $container)
27+
{
28+
$providers = array();
29+
foreach ($container->findTaggedServiceIds('container.env_provider') as $id => $tags) {
30+
foreach ($tags as $attr) {
31+
if (isset($attr['prefix'])) {
32+
$providers[$attr['prefix']] = new Reference($id);
33+
} elseif (!$r = $container->getReflectionClass($class = $container->getDefinition($id)->getClass())) {
34+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
35+
} elseif (!$r->isSubclassOf(EnvProviderInterface::class)) {
36+
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EnvProviderInterface::class));
37+
} else {
38+
foreach ($class::getProvidedTypes() as $prefix => $type) {
39+
$providers[$prefix] = new Reference($id);
40+
}
41+
}
42+
}
43+
}
44+
45+
if ($providers) {
46+
$container->register('container.env_providers_locator')
47+
->setArguments(array($providers))
48+
->addTag('container.service_locator')
49+
;
50+
}
51+
}
52+
}

src/Symfony/Component/DependencyInjection/Container.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class Container implements ResettableContainerInterface
7373
private $underscoreMap = array('_' => '', '.' => '_', '\\' => '_');
7474
private $envCache = array();
7575
private $compiled = false;
76+
private $getEnv;
7677

7778
/**
7879
* @param ParameterBagInterface $parameterBag A ParameterBagInterface instance
@@ -452,13 +453,26 @@ protected function getEnv($name)
452453
if (isset($this->envCache[$name]) || array_key_exists($name, $this->envCache)) {
453454
return $this->envCache[$name];
454455
}
455-
if (0 !== strpos($name, 'HTTP_') && isset($_SERVER[$name])) {
456-
return $this->envCache[$name] = $_SERVER[$name];
456+
if (!$this->has($id = 'container.env_providers_locator')) {
457+
$this->set($id, new ServiceLocator(array()));
457458
}
458-
if (isset($_ENV[$name])) {
459-
return $this->envCache[$name] = $_ENV[$name];
459+
if (!$this->getEnv) {
460+
$this->getEnv = new \ReflectionMethod($this, __FUNCTION__);
461+
$this->getEnv->setAccessible(true);
462+
$this->getEnv = $this->getEnv->getClosure($this);
460463
}
461-
if (false !== $env = getenv($name)) {
464+
$providers = $this->get($id);
465+
466+
if (false !== $i = strpos($name, ':')) {
467+
$prefix = substr($name, 0, $i);
468+
$localName = substr($name, 1 + $i);
469+
} else {
470+
$prefix = 'string';
471+
$localName = $name;
472+
}
473+
$provider = $providers->has($prefix) ? $providers->get($prefix) : new EnvProvider();
474+
475+
if (null !== $env = $provider->getEnv($prefix, $localName, $this->getEnv)) {
462476
return $this->envCache[$name] = $env;
463477
}
464478
if (!$this->hasParameter("env($name)")) {

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

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

1067-
if (preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $export[1])) {
1067+
if (preg_match("/\\\$this->(?:getEnv\('(?:\w++:)*+\w++'\)|targetDirs\[\d++\])/", $export[1])) {
10681068
$dynamicPhp[$key] = sprintf('%scase %s: $value = %s; break;', $export[0], $this->export($key), $export[1]);
10691069
} else {
10701070
$php[] = sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
@@ -1614,7 +1614,7 @@ private function dumpParameter($name)
16141614
return $dumpedValue;
16151615
}
16161616

1617-
if (!preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $dumpedValue)) {
1617+
if (!preg_match("/\\\$this->(?:getEnv\('(?:\w++:)*+\w++'\)|targetDirs\[\d++\])/", $dumpedValue)) {
16181618
return sprintf("\$this->parameters['%s']", $name);
16191619
}
16201620
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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;
13+
14+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
15+
16+
class EnvProvider implements EnvProviderInterface
17+
{
18+
/**
19+
* {@inheritdoc}
20+
*/
21+
public static function getProvidedTypes()
22+
{
23+
return array(
24+
'base64' => 'string',
25+
'file' => 'string',
26+
'float' => 'double',
27+
'int' => 'integer',
28+
'json' => 'array',
29+
'string' => 'string',
30+
);
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function getEnv($prefix, $name, \Closure $getEnv)
37+
{
38+
if ('base64' === $prefix) {
39+
return base64_decode($getEnv($name));
40+
}
41+
42+
if ('json' === $prefix) {
43+
$env = json_decode($getEnv($name), true, JSON_BIGINT_AS_STRING);
44+
45+
if (JSON_ERROR_NONE !== json_last_error()) {
46+
throw new RuntimeException(sprintf('Invalid JSON in env var "%s": '.json_last_error_msg(), $name));
47+
}
48+
49+
if (!is_array($env)) {
50+
throw new RuntimeException(sprintf('Invalid JSON env var "%s": array expected, %s given.', $name, gettype($env)));
51+
}
52+
53+
return $env;
54+
}
55+
56+
$i = strpos($name, ':');
57+
58+
if ('file' === $prefix) {
59+
if (0 < $i && strspn($name, '0123456789') === $i) {
60+
$maxLength = (int) $name;
61+
$name = substr($name, 1 + $i);
62+
} else {
63+
$maxLength = null;
64+
}
65+
if (!file_exists($file = $getEnv($name))) {
66+
throw new RuntimeException(sprintf('Env "file:%s" not found: %s does not exist.', $name, $file));
67+
}
68+
69+
return null === $maxLength ? file_get_contents($file) : file_get_contents($file, false, null, 0, $maxLength);
70+
}
71+
72+
if (false !== $i) {
73+
$env = $getEnv($name);
74+
} elseif (0 !== strpos($name, 'HTTP_') && isset($_SERVER[$name])) {
75+
$env = $_SERVER[$name];
76+
} elseif (isset($_ENV[$name])) {
77+
$env = $_ENV[$name];
78+
} elseif (false === $env = getenv($name)) {
79+
return;
80+
}
81+
82+
if ('string' === $prefix) {
83+
return (string) $env;
84+
}
85+
86+
if ('int' === $prefix) {
87+
if (!is_numeric($env)) {
88+
throw new RuntimeException(sprintf('Non-numeric env var "%s" cannot be cast to int.', $name));
89+
}
90+
91+
return (int) $env;
92+
}
93+
94+
if ('float' === $prefix) {
95+
if (!is_numeric($env)) {
96+
throw new RuntimeException(sprintf('Non-numeric env var "%s" cannot be cast to float.', $name));
97+
}
98+
99+
return (float) $env;
100+
}
101+
102+
throw new RuntimeException(sprintf('Unsupported env var prefix "%s".', $prefix));
103+
}
104+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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;
13+
14+
/**
15+
* The EnvProviderInterface is implemented by objects that manage environment-like variables.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
interface EnvProviderInterface
20+
{
21+
/**
22+
* Returns the value of the given variable as managed by the current instance.
23+
*
24+
* @param string $prefix The namespace of the variable
25+
* @param string $name The name of the variable within the namespace
26+
* @param \Closure $getEnv A closure that allows fetching more env vars
27+
*
28+
* @return mixed|null The value of the given variable or null when it is not found
29+
*
30+
* @throws RuntimeException on error
31+
*/
32+
public function getEnv($prefix, $name, \Closure $getEnv);
33+
34+
/**
35+
* @return string[] The PHP-types managed by getEnv(), keyed by prefixes
36+
*/
37+
public static function getProvidedTypes();
38+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function get($name)
3535
return $placeholder; // return first result
3636
}
3737
}
38-
if (preg_match('/\W/', $env)) {
38+
if (!preg_match('/^(?:\w++:)*+\w++$/', $env)) {
3939
throw new InvalidArgumentException(sprintf('Invalid %s name: only "word" characters are allowed.', $name));
4040
}
4141

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\DependencyInjection\ContainerBuilder;
2222
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
2323
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
24+
use Symfony\Component\DependencyInjection\EnvProviderInterface;
2425
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
2526
use Symfony\Component\DependencyInjection\Reference;
2627
use Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator;
@@ -338,13 +339,84 @@ public function testDumpAutowireData()
338339

339340
public function testEnvParameter()
340341
{
342+
$rand = mt_rand();
343+
putenv('Baz='.$rand);
341344
$container = new ContainerBuilder();
342345
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
343346
$loader->load('services26.yml');
347+
$container->setParameter('env(json_file)', self::$fixturesPath.'/array.json');
344348
$container->compile();
345349
$dumper = new PhpDumper($container);
346350

347-
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services26.php', $dumper->dump(), '->dump() dumps inline definitions which reference service_container');
351+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services26.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_EnvParameters', 'file' => self::$fixturesPath.'/php/services26.php')));
352+
353+
require self::$fixturesPath.'/php/services26.php';
354+
$container = new \Symfony_DI_PhpDumper_Test_EnvParameters();
355+
$this->assertSame($rand, $container->getParameter('baz'));
356+
$this->assertSame(array(123, 'abc'), $container->getParameter('json'));
357+
putenv('Baz');
358+
}
359+
360+
public function testResolvedBase64EnvParameters()
361+
{
362+
$container = new ContainerBuilder();
363+
$container->setParameter('env(foo)', base64_encode('world'));
364+
$container->setParameter('hello', '%env(base64:foo)%');
365+
$container->compile(true);
366+
367+
$expected = array(
368+
'env(foo)' => 'd29ybGQ=',
369+
'hello' => 'world',
370+
);
371+
$this->assertSame($expected, $container->getParameterBag()->all());
372+
}
373+
374+
public function testDumpedBase64EnvParameters()
375+
{
376+
$container = new ContainerBuilder();
377+
$container->setParameter('env(foo)', base64_encode('world'));
378+
$container->setParameter('hello', '%env(base64:foo)%');
379+
$container->compile();
380+
381+
$dumper = new PhpDumper($container);
382+
$dumper->dump();
383+
384+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_base64_env.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Base64Parameters')));
385+
386+
require self::$fixturesPath.'/php/services_base64_env.php';
387+
$container = new \Symfony_DI_PhpDumper_Test_Base64Parameters();
388+
$this->assertSame('world', $container->getParameter('hello'));
389+
}
390+
391+
public function testCustomEnvParameters()
392+
{
393+
$container = new ContainerBuilder();
394+
$container->setParameter('env(foo)', str_rot13('world'));
395+
$container->setParameter('hello', '%env(rot13:foo)%');
396+
$container->register(Rot13EnvProvider::class)->addTag('container.env_provider');
397+
$container->compile();
398+
399+
$dumper = new PhpDumper($container);
400+
$dumper->dump();
401+
402+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_rot13_env.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Rot13Parameters')));
403+
404+
require self::$fixturesPath.'/php/services_rot13_env.php';
405+
$container = new \Symfony_DI_PhpDumper_Test_Rot13Parameters();
406+
$this->assertSame('world', $container->getParameter('hello'));
407+
}
408+
409+
public function testFileMaxLengthParameter()
410+
{
411+
if (!file_exists('/dev/urandom')) {
412+
$this->markTestSkipped('/dev/urandom is required.');
413+
}
414+
$container = new ContainerBuilder();
415+
$container->setParameter('env(foo)', '/dev/urandom');
416+
$container->setParameter('random', '%env(file:13:foo)%');
417+
$container->compile(true);
418+
419+
$this->assertSame(13, strlen($container->getParameter('random')));
348420
}
349421

350422
/**
@@ -671,3 +743,16 @@ public function testPrivateServiceTriggersDeprecation()
671743
$container->get('bar');
672744
}
673745
}
746+
747+
class Rot13EnvProvider implements EnvProviderInterface
748+
{
749+
public function getEnv($prefix, $name, \Closure $getEnv)
750+
{
751+
return str_rot13($getEnv($name));
752+
}
753+
754+
public static function getProvidedTypes()
755+
{
756+
return array('rot13' => 'string');
757+
}
758+
}

0 commit comments

Comments
 (0)