feature #23901 [DI] Allow processing env vars (nicolas-grekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[DI] Allow processing env vars

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | see description
| License       | MIT
| Doc PR        | -

This PR is an updated version of #20276 ~~(it embeds #23899 for now.)~~

It superscedes/closes:
- [DI] Add support for secrets #23621 ping @dunglas
- Runtime container parameter not found event filter #23669 ping @marfillaster
- [DependencyInjection] [DX] Support for JSON string environment variables #23823 ping @Pierstoval
- add support for composite environment variables #17689 ping @greg0ire
- [DI] ENV parameters at runtime with PHP 7 strict types not working properly #20434 ping @sandrokeil
- Issue when using a SQLite database and the DATABASE_URL env var #23527 ping @javiereguiluz

#22151 is another story, so not fixed here.

The way it works is via `%env(foo:BAR)%` prefixes, where "foo" can be bound to any services you'd like.
By default, the following prefixes are supported:
- `bool`, `int`, `float`, `string`, `base64`
- `const` (for referencing PHP constants)
- `json` (supporting only json **arrays** for type predictability)
- `file` (eg for access to secrets stored in files.)
- `resolve` (for processing parameters inside env vars.)

New prefixes can be added by implementing the new `EnvProviderInterface`, and tagging with `container.env_provider` (see `Rot13EnvProvider` in tests.)

Prefixes can be combined to chain processing, eg.
`%env(json:base64:file:FOO)%` will be roughly equivalent to
`json_decode(base64_decode(file_get_content(getenv('FOO'))))`.

Commits
-------

1f92e459db [DI] Allow processing env vars
This commit is contained in:
Fabien Potencier 2017-09-07 07:23:45 -07:00
commit 1bfa6a9ff7
18 changed files with 978 additions and 26 deletions

View File

@ -36,6 +36,7 @@ use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
@ -283,6 +284,8 @@ class FrameworkExtension extends Extension
->addTag('console.command');
$container->registerForAutoconfiguration(ResourceCheckerInterface::class)
->addTag('config_cache.resource_checker');
$container->registerForAutoconfiguration(EnvVarProcessorInterface::class)
->addTag('container.env_var_processor');
$container->registerForAutoconfiguration(ServiceSubscriberInterface::class)
->addTag('container.service_subscriber');
$container->registerForAutoconfiguration(ArgumentValueResolverInterface::class)

View File

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

View File

@ -43,6 +43,7 @@ class PassConfig
100 => array(
$resolveClassPass = new ResolveClassPass(),
new ResolveInstanceofConditionalsPass(),
new RegisterEnvVarProcessorsPass(),
),
);

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Reference;
/**
* Creates the container.env_var_processors_locator service.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class RegisterEnvVarProcessorsPass implements CompilerPassInterface
{
private static $allowedTypes = array('array', 'bool', 'float', 'int', 'string');
public function process(ContainerBuilder $container)
{
$bag = $container->getParameterBag();
$types = array();
$processors = array();
foreach ($container->findTaggedServiceIds('container.env_var_processor') as $id => $tags) {
foreach ($tags as $attr) {
if (!$r = $container->getReflectionClass($class = $container->getDefinition($id)->getClass())) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
} elseif (!$r->isSubclassOf(EnvVarProcessorInterface::class)) {
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EnvVarProcessorInterface::class));
}
foreach ($class::getProvidedTypes() as $prefix => $type) {
$processors[$prefix] = new ServiceClosureArgument(new Reference($id));
$types[$prefix] = self::validateProvidedTypes($type, $class);
}
}
}
if ($processors) {
if ($bag instanceof EnvPlaceholderParameterBag) {
$bag->setProvidedTypes($types);
}
$container->register('container.env_var_processors_locator', ServiceLocator::class)
->setArguments(array($processors))
;
}
}
private static function validateProvidedTypes($types, $class)
{
$types = explode('|', $types);
foreach ($types as $type) {
if (!in_array($type, self::$allowedTypes)) {
throw new InvalidArgumentException(sprintf('Invalid type "%s" returned by "%s::getProvidedTypes()", expected one of "%s".', $type, $class, implode('", "', self::$allowedTypes)));
}
}
return $types;
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
@ -48,6 +49,7 @@ class Container implements ResettableContainerInterface
protected $methodMap = array();
protected $aliases = array();
protected $loading = array();
protected $resolving = array();
/**
* @internal
@ -62,6 +64,7 @@ class Container implements ResettableContainerInterface
private $underscoreMap = array('_' => '', '.' => '_', '\\' => '_');
private $envCache = array();
private $compiled = false;
private $getEnv;
/**
* @param ParameterBagInterface $parameterBag A ParameterBagInterface instance
@ -438,23 +441,37 @@ class Container implements ResettableContainerInterface
*/
protected function getEnv($name)
{
if (isset($this->resolving[$envName = "env($name)"])) {
throw new ParameterCircularReferenceException(array_keys($this->resolving));
}
if (isset($this->envCache[$name]) || array_key_exists($name, $this->envCache)) {
return $this->envCache[$name];
}
if (isset($_SERVER[$name]) && 0 !== strpos($name, 'HTTP_')) {
return $this->envCache[$name] = $_SERVER[$name];
if (!$this->has($id = 'container.env_var_processors_locator')) {
$this->set($id, new ServiceLocator(array()));
}
if (isset($_ENV[$name])) {
return $this->envCache[$name] = $_ENV[$name];
}
if (false !== ($env = getenv($name)) && null !== $env) { // null is a possible value because of thread safety issues
return $this->envCache[$name] = $env;
}
if (!$this->hasParameter("env($name)")) {
throw new EnvNotFoundException($name);
if (!$this->getEnv) {
$this->getEnv = new \ReflectionMethod($this, __FUNCTION__);
$this->getEnv->setAccessible(true);
$this->getEnv = $this->getEnv->getClosure($this);
}
$processors = $this->get($id);
return $this->envCache[$name] = $this->getParameter("env($name)");
if (false !== $i = strpos($name, ':')) {
$prefix = substr($name, 0, $i);
$localName = substr($name, 1 + $i);
} else {
$prefix = 'string';
$localName = $name;
}
$processor = $processors->has($prefix) ? $processors->get($prefix) : new EnvVarProcessor($this);
$this->resolving[$envName] = true;
try {
return $this->envCache[$name] = $processor->getEnv($prefix, $localName, $this->getEnv);
} finally {
unset($this->resolving[$envName]);
}
}
/**

View File

@ -1472,20 +1472,26 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
protected function getEnv($name)
{
$value = parent::getEnv($name);
$bag = $this->getParameterBag();
if (!is_string($value) || !$this->getParameterBag() instanceof EnvPlaceholderParameterBag) {
if (!is_string($value) || !$bag instanceof EnvPlaceholderParameterBag) {
return $value;
}
foreach ($this->getParameterBag()->getEnvPlaceholders() as $env => $placeholders) {
foreach ($bag->getEnvPlaceholders() as $env => $placeholders) {
if (isset($placeholders[$value])) {
$bag = new ParameterBag($this->getParameterBag()->all());
$bag = new ParameterBag($bag->all());
return $bag->unescapeValue($bag->get("env($name)"));
}
}
return $value;
$this->resolving["env($name)"] = true;
try {
return $bag->unescapeValue($this->resolveEnvPlaceholders($bag->escapeValue($value), true));
} finally {
unset($this->resolving["env($name)"]);
}
}
/**

View File

@ -1112,7 +1112,7 @@ EOF;
$export = $this->exportParameters(array($value));
$export = explode('0 => ', substr(rtrim($export, " )\n"), 7, -1), 2);
if (preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $export[1])) {
if (preg_match("/\\\$this->(?:getEnv\('(?:\w++:)*+\w++'\)|targetDirs\[\d++\])/", $export[1])) {
$dynamicPhp[$key] = sprintf('%scase %s: $value = %s; break;', $export[0], $this->export($key), $export[1]);
} else {
$php[] = sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
@ -1685,7 +1685,7 @@ EOF;
return $dumpedValue;
}
if (!preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $dumpedValue)) {
if (!preg_match("/\\\$this->(?:getEnv\('(?:\w++:)*+\w++'\)|targetDirs\[\d++\])/", $dumpedValue)) {
return sprintf("\$this->parameters['%s']", $name);
}
}
@ -1880,13 +1880,16 @@ EOF;
{
$export = var_export($value, true);
if ("'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$this->getEnv('%s').'")) {
if ("'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$this->getEnv('string:%s').'")) {
$export = $resolvedExport;
if ("'" === $export[1]) {
$export = substr($export, 3);
}
if (".''" === substr($export, -3)) {
$export = substr($export, 0, -3);
if ("'" === $export[1]) {
$export = substr_replace($export, '', 18, 7);
}
}
if ("'" === $export[1]) {
$export = substr($export, 3);
}
}

View File

@ -0,0 +1,160 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
class EnvVarProcessor implements EnvVarProcessorInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* {@inheritdoc}
*/
public static function getProvidedTypes()
{
return array(
'base64' => 'string',
'bool' => 'bool',
'const' => 'bool|int|float|string|array',
'file' => 'string',
'float' => 'float',
'int' => 'int',
'json' => 'array',
'resolve' => 'string',
'string' => 'string',
);
}
/**
* {@inheritdoc}
*/
public function getEnv($prefix, $name, \Closure $getEnv)
{
$i = strpos($name, ':');
if ('file' === $prefix) {
if (!is_scalar($file = $getEnv($name))) {
throw new RuntimeException(sprintf('Invalid file name: env var "%s" is non-scalar.', $name));
}
if (!file_exists($file)) {
throw new RuntimeException(sprintf('Env "file:%s" not found: %s does not exist.', $name, $file));
}
return file_get_contents($file);
}
if (false !== $i || 'string' !== $prefix) {
if (null === $env = $getEnv($name)) {
return;
}
} elseif (isset($_SERVER[$name]) && 0 !== strpos($name, 'HTTP_')) {
$env = $_SERVER[$name];
} elseif (isset($_ENV[$name])) {
$env = $_ENV[$name];
} elseif (false === ($env = getenv($name)) || null === $env) { // null is a possible value because of thread safety issues
if (!$this->container->hasParameter("env($name)")) {
throw new EnvNotFoundException($name);
}
if (null === $env = $this->container->getParameter("env($name)")) {
return;
}
}
if (!is_scalar($env)) {
throw new RuntimeException(sprintf('Non-scalar env var "%s" cannot be cast to %s.', $name, $prefix));
}
if ('string' === $prefix) {
return (string) $env;
}
if ('bool' === $prefix) {
return (bool) self::phpize($env);
}
if ('int' === $prefix) {
if (!is_numeric($env = self::phpize($env))) {
throw new RuntimeException(sprintf('Non-numeric env var "%s" cannot be cast to int.', $name));
}
return (int) $env;
}
if ('float' === $prefix) {
if (!is_numeric($env = self::phpize($env))) {
throw new RuntimeException(sprintf('Non-numeric env var "%s" cannot be cast to float.', $name));
}
return (float) $env;
}
if ('const' === $prefix) {
if (!defined($env)) {
throw new RuntimeException(sprintf('Env var "%s" maps to undefined constant "%s".', $name, $env));
}
return constant($name);
}
if ('base64' === $prefix) {
return base64_decode($env);
}
if ('json' === $prefix) {
$env = json_decode($env, true, JSON_BIGINT_AS_STRING);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new RuntimeException(sprintf('Invalid JSON in env var "%s": '.json_last_error_msg(), $name));
}
if (!is_array($env)) {
throw new RuntimeException(sprintf('Invalid JSON env var "%s": array expected, %s given.', $name, gettype($env)));
}
return $env;
}
if ('resolve' === $prefix) {
return preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($name) {
if (!isset($match[1])) {
return '%';
}
$value = $this->container->getParameter($match[1]);
if (!is_scalar($value)) {
throw new RuntimeException(sprintf('Parameter "%s" found when resolving env var "%s" must be scalar, "%s" given.', $match[1], $name, gettype($value)));
}
return $value;
}, $env);
}
throw new RuntimeException(sprintf('Unsupported env var prefix "%s".', $prefix));
}
private static function phpize($value)
{
if (!class_exists(XmlUtils::class)) {
throw new RuntimeException('The Symfony Config component is required to cast env vars to "bool", "int" or "float".');
}
return XmlUtils::phpize($value);
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection;
/**
* The EnvVarProcessorInterface is implemented by objects that manage environment-like variables.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface EnvVarProcessorInterface
{
/**
* Returns the value of the given variable as managed by the current instance.
*
* @param string $prefix The namespace of the variable
* @param string $name The name of the variable within the namespace
* @param \Closure $getEnv A closure that allows fetching more env vars
*
* @return mixed
*
* @throws RuntimeException on error
*/
public function getEnv($prefix, $name, \Closure $getEnv);
/**
* @return string[] The PHP-types managed by getEnv(), keyed by prefixes
*/
public static function getProvidedTypes();
}

View File

@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
class EnvPlaceholderParameterBag extends ParameterBag
{
private $envPlaceholders = array();
private $providedTypes = array();
/**
* {@inheritdoc}
@ -34,7 +35,7 @@ class EnvPlaceholderParameterBag extends ParameterBag
return $placeholder; // return first result
}
}
if (preg_match('/\W/', $env)) {
if (!preg_match('/^(?:\w++:)*+\w++$/', $env)) {
throw new InvalidArgumentException(sprintf('Invalid %s name: only "word" characters are allowed.', $name));
}
@ -80,6 +81,24 @@ class EnvPlaceholderParameterBag extends ParameterBag
}
}
/**
* Maps env prefixes to their corresponding PHP types.
*/
public function setProvidedTypes(array $providedTypes)
{
$this->providedTypes = $providedTypes;
}
/**
* Gets the PHP types corresponding to env() parameter prefixes.
*
* @return string[][]
*/
public function getProvidedTypes()
{
return $this->providedTypes;
}
/**
* {@inheritdoc}
*/

View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\RegisterEnvVarProcessorsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
class RegisterEnvVarProcessorsPassTest extends TestCase
{
public function testSimpleProcessor()
{
$container = new ContainerBuilder();
$container->register('foo', SimpleProcessor::class)->addTag('container.env_var_processor');
(new RegisterEnvVarProcessorsPass())->process($container);
$this->assertTrue($container->has('container.env_var_processors_locator'));
$this->assertInstanceof(SimpleProcessor::class, $container->get('container.env_var_processors_locator')->get('foo'));
$this->assertSame(array('foo' => array('string')), $container->getParameterBag()->getProvidedTypes());
}
public function testNoProcessor()
{
$container = new ContainerBuilder();
(new RegisterEnvVarProcessorsPass())->process($container);
$this->assertFalse($container->has('container.env_var_processors_locator'));
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage Invalid type "foo" returned by "Symfony\Component\DependencyInjection\Tests\Compiler\BadProcessor::getProvidedTypes()", expected one of "array", "bool", "float", "int", "string".
*/
public function testBadProcessor()
{
$container = new ContainerBuilder();
$container->register('foo', BadProcessor::class)->addTag('container.env_var_processor');
(new RegisterEnvVarProcessorsPass())->process($container);
}
}
class SimpleProcessor implements EnvVarProcessorInterface
{
public function getEnv($prefix, $name, \Closure $getEnv)
{
return $getEnv($name);
}
public static function getProvidedTypes()
{
return array('foo' => 'string');
}
}
class BadProcessor extends SimpleProcessor
{
public static function getProvidedTypes()
{
return array('foo' => 'string|foo');
}
}

View File

@ -665,6 +665,70 @@ class ContainerBuilderTest extends TestCase
$container->compile(true);
}
public function testDynamicEnv()
{
putenv('DUMMY_FOO=some%foo%');
putenv('DUMMY_BAR=%bar%');
$container = new ContainerBuilder();
$container->setParameter('foo', 'Foo%env(resolve:DUMMY_BAR)%');
$container->setParameter('bar', 'Bar');
$container->setParameter('baz', '%env(resolve:DUMMY_FOO)%');
$container->compile(true);
putenv('DUMMY_FOO');
putenv('DUMMY_BAR');
$this->assertSame('someFooBar', $container->getParameter('baz'));
}
public function testCastEnv()
{
$container = new ContainerBuilder();
$container->setParameter('env(FAKE)', '123');
$container->register('foo', 'stdClass')->setProperties(array(
'fake' => '%env(int:FAKE)%',
));
$container->compile(true);
$this->assertSame(123, $container->get('foo')->fake);
}
public function testEnvAreNullable()
{
$container = new ContainerBuilder();
$container->setParameter('env(FAKE)', null);
$container->register('foo', 'stdClass')->setProperties(array(
'fake' => '%env(int:FAKE)%',
));
$container->compile(true);
$this->assertNull($container->get('foo')->fake);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException
* @expectedExceptionMessage Circular reference detected for parameter "env(resolve:DUMMY_ENV_VAR)" ("env(resolve:DUMMY_ENV_VAR)" > "env(resolve:DUMMY_ENV_VAR)").
*/
public function testCircularDynamicEnv()
{
putenv('DUMMY_ENV_VAR=some%foo%');
$container = new ContainerBuilder();
$container->setParameter('foo', '%bar%');
$container->setParameter('bar', '%env(resolve:DUMMY_ENV_VAR)%');
try {
$container->compile(true);
} finally {
putenv('DUMMY_ENV_VAR');
}
}
/**
* @expectedException \LogicException
*/

View File

@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator;
@ -338,13 +339,82 @@ class PhpDumperTest extends TestCase
public function testEnvParameter()
{
$rand = mt_rand();
putenv('Baz='.$rand);
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services26.yml');
$container->setParameter('env(json_file)', self::$fixturesPath.'/array.json');
$container->compile();
$dumper = new PhpDumper($container);
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services26.php', $dumper->dump(), '->dump() dumps inline definitions which reference service_container');
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services26.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_EnvParameters', 'file' => self::$fixturesPath.'/php/services26.php')));
require self::$fixturesPath.'/php/services26.php';
$container = new \Symfony_DI_PhpDumper_Test_EnvParameters();
$this->assertSame($rand, $container->getParameter('baz'));
$this->assertSame(array(123, 'abc'), $container->getParameter('json'));
$this->assertSame('sqlite:///foo/bar/var/data.db', $container->getParameter('db_dsn'));
putenv('Baz');
}
public function testResolvedBase64EnvParameters()
{
$container = new ContainerBuilder();
$container->setParameter('env(foo)', base64_encode('world'));
$container->setParameter('hello', '%env(base64:foo)%');
$container->compile(true);
$expected = array(
'env(foo)' => 'd29ybGQ=',
'hello' => 'world',
);
$this->assertSame($expected, $container->getParameterBag()->all());
}
public function testDumpedBase64EnvParameters()
{
$container = new ContainerBuilder();
$container->setParameter('env(foo)', base64_encode('world'));
$container->setParameter('hello', '%env(base64:foo)%');
$container->compile();
$dumper = new PhpDumper($container);
$dumper->dump();
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_base64_env.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Base64Parameters')));
require self::$fixturesPath.'/php/services_base64_env.php';
$container = new \Symfony_DI_PhpDumper_Test_Base64Parameters();
$this->assertSame('world', $container->getParameter('hello'));
}
public function testCustomEnvParameters()
{
$container = new ContainerBuilder();
$container->setParameter('env(foo)', str_rot13('world'));
$container->setParameter('hello', '%env(rot13:foo)%');
$container->register(Rot13EnvVarProcessor::class)->addTag('container.env_var_processor');
$container->compile();
$dumper = new PhpDumper($container);
$dumper->dump();
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_rot13_env.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Rot13Parameters')));
require self::$fixturesPath.'/php/services_rot13_env.php';
$container = new \Symfony_DI_PhpDumper_Test_Rot13Parameters();
$this->assertSame('world', $container->getParameter('hello'));
}
public function testFileEnvProcessor()
{
$container = new ContainerBuilder();
$container->setParameter('env(foo)', __FILE__);
$container->setParameter('random', '%env(file:foo)%');
$container->compile(true);
$this->assertStringEqualsFile(__FILE__, $container->getParameter('random'));
}
/**
@ -360,6 +430,31 @@ class PhpDumperTest extends TestCase
$dumper->dump();
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException
* @expectedExceptionMessage Circular reference detected for parameter "env(resolve:DUMMY_ENV_VAR)" ("env(resolve:DUMMY_ENV_VAR)" > "env(resolve:DUMMY_ENV_VAR)").
*/
public function testCircularDynamicEnv()
{
$container = new ContainerBuilder();
$container->setParameter('foo', '%bar%');
$container->setParameter('bar', '%env(resolve:DUMMY_ENV_VAR)%');
$container->compile();
$dumper = new PhpDumper($container);
$dump = $dumper->dump(array('class' => $class = __FUNCTION__));
eval('?>'.$dump);
$container = new $class();
putenv('DUMMY_ENV_VAR=%foo%');
try {
$container->getParameter('bar');
} finally {
putenv('DUMMY_ENV_VAR');
}
}
public function testInlinedDefinitionReferencingServiceContainer()
{
$container = new ContainerBuilder();
@ -748,3 +843,16 @@ class PhpDumperTest extends TestCase
$this->assertSame('bar', $container->getParameter('FOO'));
}
}
class Rot13EnvVarProcessor implements EnvVarProcessorInterface
{
public function getEnv($prefix, $name, \Closure $getEnv)
{
return str_rot13($getEnv($name));
}
public static function getProvidedTypes()
{
return array('rot13' => 'string');
}
}

View File

@ -0,0 +1 @@
[123, "abc"]

View File

@ -9,14 +9,14 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* ProjectServiceContainer.
* Symfony_DI_PhpDumper_Test_EnvParameters.
*
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*
* @final since Symfony 3.3
*/
class ProjectServiceContainer extends Container
class Symfony_DI_PhpDumper_Test_EnvParameters extends Container
{
private $parameters;
private $targetDirs = array();
@ -26,6 +26,10 @@ class ProjectServiceContainer extends Container
*/
public function __construct()
{
$dir = __DIR__;
for ($i = 1; $i <= 5; ++$i) {
$this->targetDirs[$i] = $dir = dirname($dir);
}
$this->parameters = $this->getDefaultParameters();
$this->services = array();
@ -71,7 +75,7 @@ class ProjectServiceContainer extends Container
{
$class = $this->getEnv('FOO');
return $this->services['test'] = new $class($this->getEnv('Bar'), 'foo'.$this->getEnv('FOO').'baz');
return $this->services['test'] = new $class($this->getEnv('Bar'), 'foo'.$this->getEnv('string:FOO').'baz', $this->getEnv('int:Baz'));
}
/**
@ -129,6 +133,10 @@ class ProjectServiceContainer extends Container
private $loadedDynamicParameters = array(
'bar' => false,
'baz' => false,
'json' => false,
'db_dsn' => false,
'env(json_file)' => false,
);
private $dynamicParameters = array();
@ -145,6 +153,10 @@ class ProjectServiceContainer extends Container
{
switch ($name) {
case 'bar': $value = $this->getEnv('FOO'); break;
case 'baz': $value = $this->getEnv('int:Baz'); break;
case 'json': $value = $this->getEnv('json:file:json_file'); break;
case 'db_dsn': $value = $this->getEnv('resolve:DB'); break;
case 'env(json_file)': $value = ($this->targetDirs[1].'/array.json'); break;
default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name));
}
$this->loadedDynamicParameters[$name] = true;
@ -154,6 +166,7 @@ class ProjectServiceContainer extends Container
private $normalizedParameterNames = array(
'env(foo)' => 'env(FOO)',
'env(db)' => 'env(DB)',
);
private function normalizeParameterName($name)
@ -178,7 +191,9 @@ class ProjectServiceContainer extends Container
protected function getDefaultParameters()
{
return array(
'project_dir' => '/foo/bar',
'env(FOO)' => 'foo',
'env(DB)' => 'sqlite://%project_dir%/var/data.db',
);
}
}

View File

@ -0,0 +1,167 @@
<?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* Symfony_DI_PhpDumper_Test_Base64Parameters.
*
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*
* @final since Symfony 3.3
*/
class Symfony_DI_PhpDumper_Test_Base64Parameters extends Container
{
private $parameters;
private $targetDirs = array();
/**
* Constructor.
*/
public function __construct()
{
$this->parameters = $this->getDefaultParameters();
$this->services = array();
$this->aliases = array();
}
/**
* {@inheritdoc}
*/
public function compile()
{
throw new LogicException('You cannot compile a dumped container that was already compiled.');
}
/**
* {@inheritdoc}
*/
public function isCompiled()
{
return true;
}
/**
* {@inheritdoc}
*/
public function isFrozen()
{
@trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Use the isCompiled() method instead.', __METHOD__), E_USER_DEPRECATED);
return true;
}
/**
* {@inheritdoc}
*/
public function getParameter($name)
{
if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) {
$name = $this->normalizeParameterName($name);
if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) {
throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name));
}
}
if (isset($this->loadedDynamicParameters[$name])) {
return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name);
}
return $this->parameters[$name];
}
/**
* {@inheritdoc}
*/
public function hasParameter($name)
{
$name = $this->normalizeParameterName($name);
return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters);
}
/**
* {@inheritdoc}
*/
public function setParameter($name, $value)
{
throw new LogicException('Impossible to call set() on a frozen ParameterBag.');
}
/**
* {@inheritdoc}
*/
public function getParameterBag()
{
if (null === $this->parameterBag) {
$parameters = $this->parameters;
foreach ($this->loadedDynamicParameters as $name => $loaded) {
$parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name);
}
$this->parameterBag = new FrozenParameterBag($parameters);
}
return $this->parameterBag;
}
private $loadedDynamicParameters = array(
'hello' => false,
);
private $dynamicParameters = array();
/**
* Computes a dynamic parameter.
*
* @param string The name of the dynamic parameter to load
*
* @return mixed The value of the dynamic parameter
*
* @throws InvalidArgumentException When the dynamic parameter does not exist
*/
private function getDynamicParameter($name)
{
switch ($name) {
case 'hello': $value = $this->getEnv('base64:foo'); break;
default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name));
}
$this->loadedDynamicParameters[$name] = true;
return $this->dynamicParameters[$name] = $value;
}
private $normalizedParameterNames = array();
private function normalizeParameterName($name)
{
if (isset($this->normalizedParameterNames[$normalizedName = strtolower($name)]) || isset($this->parameters[$normalizedName]) || array_key_exists($normalizedName, $this->parameters)) {
$normalizedName = isset($this->normalizedParameterNames[$normalizedName]) ? $this->normalizedParameterNames[$normalizedName] : $normalizedName;
if ((string) $name !== $normalizedName) {
@trigger_error(sprintf('Parameter names will be made case sensitive in Symfony 4.0. Using "%s" instead of "%s" is deprecated since version 3.4.', $name, $normalizedName), E_USER_DEPRECATED);
}
} else {
$normalizedName = $this->normalizedParameterNames[$normalizedName] = (string) $name;
}
return $normalizedName;
}
/**
* Gets the default parameters.
*
* @return array An array of the default parameters
*/
protected function getDefaultParameters()
{
return array(
'env(foo)' => 'd29ybGQ=',
);
}
}

View File

@ -0,0 +1,196 @@
<?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* Symfony_DI_PhpDumper_Test_Rot13Parameters.
*
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*
* @final since Symfony 3.3
*/
class Symfony_DI_PhpDumper_Test_Rot13Parameters extends Container
{
private $parameters;
private $targetDirs = array();
/**
* Constructor.
*/
public function __construct()
{
$this->parameters = $this->getDefaultParameters();
$this->services = array();
$this->normalizedIds = array(
'symfony\\component\\dependencyinjection\\tests\\dumper\\rot13envvarprocessor' => 'Symfony\\Component\\DependencyInjection\\Tests\\Dumper\\Rot13EnvVarProcessor',
);
$this->methodMap = array(
'Symfony\\Component\\DependencyInjection\\Tests\\Dumper\\Rot13EnvVarProcessor' => 'getRot13EnvVarProcessorService',
'container.env_var_processors_locator' => 'getContainer_EnvVarProcessorsLocatorService',
);
$this->aliases = array();
}
/**
* {@inheritdoc}
*/
public function compile()
{
throw new LogicException('You cannot compile a dumped container that was already compiled.');
}
/**
* {@inheritdoc}
*/
public function isCompiled()
{
return true;
}
/**
* {@inheritdoc}
*/
public function isFrozen()
{
@trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Use the isCompiled() method instead.', __METHOD__), E_USER_DEPRECATED);
return true;
}
/**
* Gets the public 'Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor' shared service.
*
* @return \Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor
*/
protected function getRot13EnvVarProcessorService()
{
return $this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor();
}
/**
* Gets the public 'container.env_var_processors_locator' shared service.
*
* @return \Symfony\Component\DependencyInjection\ServiceLocator
*/
protected function getContainer_EnvVarProcessorsLocatorService()
{
return $this->services['container.env_var_processors_locator'] = new \Symfony\Component\DependencyInjection\ServiceLocator(array('rot13' => function () {
return ${($_ = isset($this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor']) ? $this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] : $this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor()) && false ?: '_'};
}));
}
/**
* {@inheritdoc}
*/
public function getParameter($name)
{
if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) {
$name = $this->normalizeParameterName($name);
if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) {
throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name));
}
}
if (isset($this->loadedDynamicParameters[$name])) {
return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name);
}
return $this->parameters[$name];
}
/**
* {@inheritdoc}
*/
public function hasParameter($name)
{
$name = $this->normalizeParameterName($name);
return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters);
}
/**
* {@inheritdoc}
*/
public function setParameter($name, $value)
{
throw new LogicException('Impossible to call set() on a frozen ParameterBag.');
}
/**
* {@inheritdoc}
*/
public function getParameterBag()
{
if (null === $this->parameterBag) {
$parameters = $this->parameters;
foreach ($this->loadedDynamicParameters as $name => $loaded) {
$parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name);
}
$this->parameterBag = new FrozenParameterBag($parameters);
}
return $this->parameterBag;
}
private $loadedDynamicParameters = array(
'hello' => false,
);
private $dynamicParameters = array();
/**
* Computes a dynamic parameter.
*
* @param string The name of the dynamic parameter to load
*
* @return mixed The value of the dynamic parameter
*
* @throws InvalidArgumentException When the dynamic parameter does not exist
*/
private function getDynamicParameter($name)
{
switch ($name) {
case 'hello': $value = $this->getEnv('rot13:foo'); break;
default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name));
}
$this->loadedDynamicParameters[$name] = true;
return $this->dynamicParameters[$name] = $value;
}
private $normalizedParameterNames = array();
private function normalizeParameterName($name)
{
if (isset($this->normalizedParameterNames[$normalizedName = strtolower($name)]) || isset($this->parameters[$normalizedName]) || array_key_exists($normalizedName, $this->parameters)) {
$normalizedName = isset($this->normalizedParameterNames[$normalizedName]) ? $this->normalizedParameterNames[$normalizedName] : $normalizedName;
if ((string) $name !== $normalizedName) {
@trigger_error(sprintf('Parameter names will be made case sensitive in Symfony 4.0. Using "%s" instead of "%s" is deprecated since version 3.4.', $name, $normalizedName), E_USER_DEPRECATED);
}
} else {
$normalizedName = $this->normalizedParameterNames[$normalizedName] = (string) $name;
}
return $normalizedName;
}
/**
* Gets the default parameters.
*
* @return array An array of the default parameters
*/
protected function getDefaultParameters()
{
return array(
'env(foo)' => 'jbeyq',
);
}
}

View File

@ -1,6 +1,11 @@
parameters:
project_dir: '/foo/bar'
env(FOO): foo
env(DB): 'sqlite://%%project_dir%%/var/data.db'
bar: '%env(FOO)%'
baz: '%env(int:Baz)%'
json: '%env(json:file:json_file)%'
db_dsn: '%env(resolve:DB)%'
services:
test:
@ -8,3 +13,4 @@ services:
arguments:
- '%env(Bar)%'
- 'foo%bar%baz'
- '%baz%'