8000 [DI] Allow processing env vars - handles int/float/file/array/base64 … · symfony/symfony@d75dcae · GitHub
[go: up one dir, main page]

Skip to content

Commit d75dcae

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

File tree

13 files changed

+618
-12
lines changed

13 files changed

+618
-12
lines changed

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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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\Reference;
16+
17+
/**
18+
* Creates the container.env_providers_locator service.
19+
*
20+
* @author Nicolas Grekas <p@tchwork.com>
21+
*/
22+
class RegisterEnvProvidersPass implements CompilerPassInterface
23+
{
24+
public function process(ContainerBuilder $container)
25+
{
26+
$providers = array();
27+
foreach ($container->findTaggedServiceIds('container.env_provider') as $id => $tags) {
28+
foreach ($tags as $attr) {
29+
if (!isset($attr['prefix'])) {
30+
throw new InvalidArgumentException(sprintf('Missing tag attribute "prefix" on container.env_provider service "%s".', $id));
31+
}
32+
33+
$providers[$attr['prefix']] = new Reference($id);
34+
}
35+
}
36+
37+
if ($providers) {
38+
$container->register('container.env_providers_locator')
39+
->setArguments(array($providers))
40+
->addTag('container.service_locator')
41+
;
42+
}
43+
}
44+
}

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: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 function getEnv($prefix, $name, \Closure $getEnv)
22+
{
23+
if ('base64' === $prefix) {
24+
return base64_decode($getEnv($name));
25+
}
26+
27+
if ('json' === $prefix) {
28+
$env = json_decode($getEnv($name), true, JSON_BIGINT_AS_STRING);
29+
30+
if (JSON_ERROR_NONE !== json_last_error()) {
31+
throw new RuntimeException(sprintf('Invalid JSON in env var "%s": '.json_last_error_msg(), $name));
32+
}
33+
34+
if (!is_array($env)) {
35+
throw new RuntimeException(sprintf('Invalid JSON env var "%s": array expected, %s given.', $name, gettype($env)));
36+
}
37+
38+
return $env;
39+
}
40+
41+
$i = strpos($name, ':');
42+
43+
if ('file' === $prefix) {
44+
if (0 < $i && strspn($name, '0123456789') === $i) {
45+
$maxLength = (int) $name;
46+
$name = substr($name, 1 + $i);
47+
} else {
48+
$maxLength = null;
49+
}
50+
if (!file_exists($file = $getEnv($name))) {
51+
throw new RuntimeException(sprintf('Env "file:%s" not found: %s does not exist.', $name, $file));
52+
}
53+
54+
return null === $maxLength ? file_get_contents($file) : file_get_contents($file, false, null, 0, $maxLength);
55+
}
56+
57+
if (false !== $i) {
58+
$env = $getEnv($name);
59+
} elseif (0 !== strpos($name, 'HTTP_') && isset($_SERVER[$name])) {
60+
$env = $_SERVER[$name];
61+
} elseif (isset($_ENV[$name])) {
62+
$env = $_ENV[$name];
63+
} elseif (false === $env = getenv($name)) {
64+
return;
65+
}
66+
67+
if ('string' === $prefix) {
68+
return (string) $env;
69+
}
70+
71+
if ('int' === $prefix) {
72+
if (!is_numeric($env)) {
73+
throw new RuntimeException(sprintf('Non-numeric env var "%s" cannot be cast to int.', $name));
74+
}
75+
76+
return (int) $env;
77+
}
78+
79+
if ('float' === $prefix) {
80+
if (!is_numeric($env)) {
81+
throw new RuntimeException(sprintf('Non-numeric env var "%s" cannot be cast to int.', $name));
82+
}
83+
84+
return (float) $env;
85+
}
86+
87+
throw new RuntimeException(sprintf('Unsupported env var prefix "%s".', $prefix));
88+
}
89+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
}

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: 81 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', array('prefix' => 'rot13'));
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,11 @@ 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+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[123, "abc"]

0 commit comments

Comments
 (0)
0