10000 [DI] Allow fetching env vars with lookup-dedicated services · symfony/symfony@105c09b · GitHub
[go: up one dir, main page]

Skip to content

Commit 105c09b

Browse files
[DI] Allow fetching env vars with lookup-dedicated services
1 parent 13265ae commit 105c09b

File tree

13 files changed

+238
-7
lines changed

13 files changed

+238
-7
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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\Exception\InvalidArgumentException;
16+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
17+
use Symfony\Component\DependencyInjection\GetEnvInterface;
18+
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
19+
20+
/**
21+
* Checks that all env-referenced services exist and implement GetEnvInterface.
22+
*
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*/
25+
class CheckEnvReferencedServicesPass implements CompilerPassInterface
26+
{
27+
public function process(ContainerBuilder $container)
28+
{
29+
$envReferencedServices = $container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $container->getParameterBag()->getEnvReferencedServices() : array();
30+
31+
foreach ($envReferencedServices as $id) {
32+
if (!$container->has($id)) {
33+
throw new ServiceNotFoundException($id);
34+
}
35+
$class = $container->getDefinition($id)->getClass();
36+
if (!is_subclass_of($class, GetEnvInterface::class)) {
37+
if (!class_exists($class, false)) {
38+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
39+
}
40+
41+
throw new InvalidArgumentException(sprintf('The service "%s" referenced in env parameters must implement "%s".', $id, GetEnvInterface::class));
42+
}
43+
}
44+
}
45+
}

src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
2727
private $compiler;
2828
private $formatter;
2929
private $currentId;
30+
private $envReferencedServices;
3031

3132
/**
3233
* {@inheritdoc}
@@ -46,6 +47,7 @@ public function process(ContainerBuilder $container)
4647
$this->compiler = $container->getCompiler();
4748
$this->formatter = $this->compiler->getLoggingFormatter();
4849
$this->graph = $this->compiler->getServiceReferenceGraph();
50+
$this->envReferencedServices = $container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $container->getParameterBag()->getEnvReferencedServices() : array();
4951

5052
$container->setDefinitions($this->inlineArguments($container, $container->getDefinitions(), true));
5153
}
@@ -107,6 +109,10 @@ private function inlineArguments(ContainerBuilder $container, array $arguments,
107109
*/
108110
private function isInlineableDefinition($id, Definition $definition)
109111
{
112+
if (isset($this->envReferencedServices[$id])) {
113+
return false;
114+
}
115+
110116
if (!$definition->isShared()) {
111117
return true;
112118
}

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ public function __construct()
6666
)),
6767
new CheckExceptionOnInvalidReferenceBehaviorPass(),
6868
));
69+
70+
$this->afterRemovingPasses = array(array(
71+
new CheckEnvReferencedServicesPass(),
72+
));
6973
}
7074

7175
/**

src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

1414
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
1516

1617
/**
1718
* Removes unused service definitions from the container.
@@ -40,10 +41,11 @@ public function process(ContainerBuilder $container)
4041
$compiler = $container->getCompiler();
4142
$formatter = $compiler->getLoggingFormatter();
4243
$graph = $compiler->getServiceReferenceGraph();
44+
$envReferencedServices = $container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $container->getParameterBag()->getEnvReferencedServices() : array();
4345

4446
$hasChanged = false;
4547
foreach ($container->getDefinitions() as $id => $definition) {
46-
if ($definition->isPublic()) {
48+
if ($definition->isPublic() || isset($envReferencedServices[$id])) {
4749
continue;
4850
}
4951

src/Symfony/Component/DependencyInjection/Container.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ public static function underscore($id)
404404
*
405405
* @param string The name of the environment variable
406406
*
407-
* @return scalar The value to use for the provided environment variable name
407+
* @return mixed The value to use for the provided environment variable
408408
*
409409
* @throws EnvNotFoundException When the environment variable is not found and has no default value
410410
*/
@@ -419,6 +419,14 @@ protected function getEnv($name)
419419
if (false !== $env = getenv($name)) {
420420
return $this->envCache[$name] = $env;
421421
}
422+
if (false !== $i = strpos($name, '@')) {
423+
$id = substr($name, 1 + $i);
424+
$service = isset($this->services[$id]) ? $this->services[$id] : (isset($this->methodMap[$id]) ? $this->{$this->methodMap[$id]}() : $this->get($id));
425+
426+
if (null !== $env = $service->getEnv(substr($name, 0, $i))) {
427+
return $this->envCache[$name] = $env;
428+
}
429+
}
422430
if (!$this->hasParameter("env($name)")) {
423431
throw new EnvNotFoundException($name);
424432
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ private function addDefaultParametersMethod()
10041004
$export = $this->exportParameters(array($value));
10051005
$export = explode('0 => ', substr(rtrim($export, " )\n"), 7, -1), 2);
10061006

1007-
if (preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $export[1])) {
1007+
if (preg_match("/\\\$this->(?:getEnv\('[-.a-zA-Z0-9_\x7f-\xff]++(?:@[-.a-zA-Z0-9_\x7f-\xff]++)?'\)|targetDirs\[\d++\])/", $export[1])) {
10081008
$dynamicPhp[$key] = sprintf('%scase %s: $value = %s; break;', $export[0], $this->export($key), $export[1]);
10091009
} else {
10101010
$php[] = sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 GetEnvInterface is implemented by objects that manage environment-like variables.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
interface GetEnvInterface
20+
{
21+
/**
22+
* Returns the value of the given variable as managed by the current instance.
23+
*
24+
* @param string $name The name of the variable
25+
*
26+
* @return mixed|null The value of the given variable or null when it is not found
27+
*/
28+
public function getEnv($name);
29+
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
class EnvPlaceholderParameterBag extends ParameterBag
2121
{
2222
private $envPlaceholders = array();
23+
private $envReferencedServices = array();
2324

2425
/**
2526
* {@inheritdoc}
@@ -34,7 +35,7 @@ public function get($name)
3435
return $placeholder; // return first result
3536
}
3637
}
37-
if (preg_match('/\W/', $env)) {
38+
if (!preg_match('/^([-.a-zA-Z0-9_\x7f-\xff]++)(?:@([-.a-zA-Z0-9_\x7f-\xff]++))?$/', $env, $match)) {
3839
throw new InvalidArgumentException(sprintf('Invalid %s name: only "word" characters are allowed.', $name));
3940
}
4041

@@ -45,6 +46,11 @@ public function get($name)
4546
throw new RuntimeException(sprintf('The default value of an env() parameter must be scalar or null, but "%s" given to "%s".', gettype($defaultValue), $name));
4647
}
4748
}
49+
if (isset($match[2])) {
50+
$serviceId = strtolower($match[2]);
51+
$this->envReferencedServices[$serviceId] = $serviceId;
52+
$env = $match[1].'@'.$serviceId;
53+
}
4854

4955
$uniqueName = md5($name.uniqid(mt_rand(), true));
5056
$placeholder = sprintf('env_%s_%s', $env, $uniqueName);
@@ -66,6 +72,16 @@ public function getEnvPlaceholders()
6672
return $this->envPlaceholders;
6773
}
6874

75+
/**
76+
* Returns the list of services referenced in `env()` parameters.
77+
*
78+
* @return string[] The list of services referenced in `env()` parameters
79+
*/
80+
public function getEnvReferencedServices()
81+
{
82+
return $this->envReferencedServices;
83+
}
84+
6985
/**
7086
* Merges the env placeholders of another EnvPlaceholderParameterBag.
7187
*/
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\Tests\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CheckEnvReferencedServicesPass;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\GetEnvInterface;
17+
18+
class CheckEnvReferencedServicePassTest extends \PHPUnit_Framework_TestCase
19+
{
20+
public function testProcess()
21+
{
22+
$container = new ContainerBuilder();
23+
$container->getParameterBag()->get('env(foo@a)');
24+
$container->register('a', GetEnvService::class);
25+
26+
$pass = new CheckEnvReferencedServicesPass();
27+
$pass->process($container);
28+
}
29+
30+
/**
31+
* @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
32+
*/
33+
public function testProcessThrowsExceptionOnInvalidReference()
34+
{
35+
$container = new ContainerBuilder();
36+
$container->getParameterBag()->get('env(foo@a)');
37+
38+
$pass = new CheckEnvReferencedServicesPass();
39+
$pass->process($container);
40+
}
41+
42+
/**
43+
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
44+
*/
45+
public function testProcessThrowsExceptionOnInvalidReferenceFromInlinedDefinition()
46+
{
47+
$container = new ContainerBuilder();
48+
$container->getParameterBag()->get('env(foo@a)');
49+
$container->register('a', \stdClass::class);
50+
51+
$pass = new CheckEnvReferencedServicesPass();
52+
$pass->process($container);
53+
}
54+
}
55+
56+
class GetEnvService implements GetEnvInterface
57+
{
58+
public function getEnv($name)
59+
{
60+
return $name.$name;
61+
}
62+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Config\FileLocator;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
1616
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
17+
use Symfony\Component\DependencyInjection\GetEnvInterface;
1718
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
1819
use Symfony\Component\DependencyInjection\Reference;
1920
use Symfony\Component\DependencyInjection\Definition;
@@ -295,7 +296,11 @@ public function testEnvParameter()
295296
$container->compile();
296297
$dumper = new PhpDumper($container);
297298

298-
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services26.php', $dumper->dump(), '->dump() dumps inline definitions which reference service_container');
299+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services26.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_EnvParameters')), '->dump() dumps inline definitions which reference service_container');
300+
301+
require self::$fixturesPath.'/php/services26.php';
302+
$container = new \Symfony_DI_PhpDumper_Test_EnvParameters();
303+
$this->assertSame('BAZ123', $container->getParameter('baz'));
299304
}
300305

301306
/**
@@ -341,3 +346,11 @@ public function testInitializePropertiesBeforeMethodCalls()
341346
$this->assertTrue($container->get('bar')->callPassed(), '->dump() initializes properties before method calls');
342347
}
343348
}
349+
350+
class EnvResolver implements GetEnvInterface
351+
{
352+
public function getEnv($name)
353+
{
354+
return strtoupper($name).'123';
355+
}
356+
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services26.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
99

1010
/**
11-
* ProjectServiceContainer.
11+
* Symfony_DI_PhpDumper_Test_EnvParameters.
1212
*
1313
* This class has been auto-generated
1414
* by the Symfony Dependency Injection Component.
1515
*/
16-
class ProjectServiceContainer extends Container
16+
class Symfony_DI_PhpDumper_Test_EnvParameters extends Container
1717
{
1818
private $parameters;
1919
private $targetDirs = array();
@@ -27,6 +27,7 @@ public function __construct()
2727

2828
$this->services = array();
2929
$this->methodMap = array(
30+
'env_resolver' => 'getEnvResolverService',
3031
'test' => 'getTestService',
3132
);
3233

@@ -64,6 +65,23 @@ protected function getTestService()
6465
return $this->services['test'] = new $class($this->getEnv('Bar'), 'foo'.$this->getEnv('FOO').'baz');
6566
}
6667

68+
/**
69+
* Gets the 'env_resolver' service.
70+
*
71+
* This service is shared.
72+
* This method always returns the same instance of the service.
73+
*
74+
* This service is private.
75+
* If you want to be able to request this service from the container directly,
76+
* make it public, otherwise you might end up with broken code.
77+
*
78+
* @return \Symfony\Component\DependencyInjection\Tests\Dumper\EnvResolver A Symfony\Component\DependencyInjection\Tests\Dumper\EnvResolver instance
79+
*/
80+
protected function getEnvResolverService()
81+
{
82+
return $this->services['env_resolver'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\EnvResolver();
83+
}
84+
6785
/**
6886
* {@inheritdoc}
6987
*/
@@ -117,6 +135,7 @@ public function getParameterBag()
117135

118136
private $loadedDynamicParameters = array(
119137
'bar' => false,
138+
'baz' => false,
120139
);
121140
private $dynamicParameters = array();
122141

@@ -133,6 +152,7 @@ private function getDynamicParameter($name)
133152
{
134153
switch ($name) {
135154
case 'bar': $value = $this->getEnv('FOO'); break;
155+
case 'baz': $value = $this->getEnv('Baz@env_resolver'); break;
136156
default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name));
137157
}
138158
$this->loadedDynamicParameters[$name] = true;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
parameters:
22
env(FOO): foo
33
bar: '%env(FOO)%'
4+
baz: '%env(Baz@Env_Resolver)%'
45

56
services:
67
test:
78
class: '%env(FOO)%'
89
arguments:
910
- '%env(Bar)%'
1011
- 'foo%bar%baz'
12+
env_resolver:
13+
class: 'Symfony\Component\DependencyInjection\Tests\Dumper\EnvResolver'
14+
public: false

0 commit comments

Comments
 (0)
0