8000 [DI] Allow fetching env vars with lookup-dedicated services by nicolas-grekas · Pull Request #20276 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[DI] Allow fetching env vars with lookup-dedicated services #20276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"phpdocumentor/reflection-docblock": "^3.0"
},
"conflict": {
"phpdocumentor/reflection-docblock": "<3.0||>=3.2.0,<3.2.1",
"phpdocumentor/reflection-docblock": "<3.0||>=3.2.0,<3.2.2",
"phpdocumentor/type-resolver": "<0.2.0",
"phpunit/phpunit": "<4.8.35|<5.4.3,&g 8000 t;=5.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\GetEnvInterface;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;

/**
* Checks that all env-referenced services exist and implement GetEnvInterface.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class CheckEnvReferencedServicesPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$envReferencedServices = $container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $container->getParameterBag()->getEnvReferencedServices() : array();

foreach ($envReferencedServices as $id) {
if (!$container->has($id)) {
throw new ServiceNotFoundException($id);
}
$class = $container->getDefinition($id)->getClass();
if (!is_subclass_of($class, GetEnvInterface::class)) {
if (!class_exists($class, false)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}

throw new InvalidArgumentException(sprintf('The service "%s" referenced in env parameters must implement "%s".', $id, GetEnvInterface::class));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\Reference;

/**
Expand Down Expand Up @@ -107,6 +108,10 @@ private function isInlineableDefinition($id, Definition $definition, ServiceRefe
return false;
}

if (($bag = $this->container->getParameterBag)() instanceof EnvPlaceholderParameterBag && isset($bag->getEnvReferencedServices()[$id])) {
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ public function __construct()
new AutowireExceptionPass($autowirePass, $inlinedServicePass),
new CheckExceptionOnInvalidReferenceBehaviorPass(),
));

$this->afterRemovingPasses = array(array(
new CheckEnvReferencedServicesPass(),
));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;

/**
* Removes unused service definitions from the container.
Expand All @@ -38,10 +39,11 @@ public function setRepeatedPass(RepeatedPass $repeatedPass)
public function process(ContainerBuilder $container)
{
$graph = $container->getCompiler()->getServiceReferenceGraph();
$envReferencedServices = $container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $container->getParameterBag()->getEnvReferencedServices() : array();

$hasChanged = false;
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isPublic()) {
if ($definition->isPublic() || isset($envReferencedServices[$id])) {
continue;
}

Expand Down
10 changes: 9 additions & 1 deletion src/Symfony/Component/DependencyInjection/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ protected function load($file)
*
* @param string The name of the environment variable
*
* @return scalar The value to use for the provided environment variable name
* @return mixed The value to use for the provided environment variable
*
* @throws EnvNotFoundException When the environment variable is not found and has no default value
*/
Expand All @@ -458,6 +458,14 @@ protected function getEnv($name)
if (false !== $env = getenv($name)) {
return $this->envCache[$name] = $env;
}
if (false !== $i = strpos($name, '@')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a default implementation of GetEnvInterface? and be explicit here (in terms of priority);

if () {
   $service->getEnv();
} else {
   $default->getEnv();
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a default here: it's the code surrounding the new block.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean a GetEnv class for the getenv/$_ENV lookup.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's what I thought, and that would require a class, which means a service, thus a service definition. But the container comes without any service preconfigured...

Copy link
Contributor
@ro0NL ro0NL 4D24 Oct 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is about if we should allow to override the lookup service by having FOO@service in our $_ENV var for example. I've no strong opinion on this, but it could make sense to do:

if (false !== $i = strpos($name, '@')) {
    $value = $service->getEnv(substr($name, 0, $i));
} else {
    $value = (new GetEnv())->getEnv($name);
}

Ie. consistently rely on 1 lookup implementation.

Copy link
Contributor
@ro0NL ro0NL Oct 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and shouldnt this be strrpos? Not sure ;-)

edit: never mind, it's no expected value anyway.

$id = substr($name, 1 + $i);
$service = isset($this->services[$id]) ? $this->services[$id] : (isset($this->methodMap[$id]) ? $this->{$this->methodMap[$id]}() : $this->get($id));

if (null !== $env = $service->getEnv(substr($name, 0, $i))) {
return $this->envCache[$name] = $env;
}
}
if (!$this->hasParameter("env($name)")) {
Copy link
Contributor
@ro0NL ro0NL Oct 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldnt we use the normalized name here? And provide a default value regarding the lookup service?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By normalized name, you mean with lowercased service id? If yes, it's not required: parameters are already case insensitive in the base ParameterBag.
And no, we don't want a default service: the container comes with no service by default, yet env vars should work. See comment above.

Copy link
Contributor
@ro0NL ro0NL Oct 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean the normalized key: substr($name, 0, $i), ie.

env(FOO): default
env(FOO@service): default

The first could work in all cases right? Making the second obsolete.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But these are really two separate variable identifiers.

Copy link
Contributor
@ro0NL ro0NL Oct 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, lets stay true to the parameter architecture. (opposed to what im thinking towards;)

parameters:
   key: value

env:
   FOO: bar

throw new EnvNotFoundException($name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ private function addDefaultParametersMethod()
$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\('[-.a-zA-Z0-9_\x7f-\xff]++(?:@[-.a-zA-Z0-9_\x7f-\xff]++)?'\)|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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ private function addService($id, $definition)
$code .= sprintf(" autowiring_types:\n%s", $autowiringTypesCode);
}

if ($definition->isAutoconfigured()) {
$code .= " autoconfigure: true\n";
}

if ($definition->isAbstract()) {
$code .= " abstract: true\n";
}

if ($definition->isLazy()) {
$code .= " lazy: true\n";
}
Expand Down
29 changes: 29 additions & 0 deletions src/Symfony/Component/DependencyInjection/GetEnvInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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 GetEnvInterface is implemented by objects that manage environment-like variables.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface GetEnvInterface
{
/**
* Returns the value of the given variable as managed by the current instance.
*
* @param string $name The name of the variable
*
* @return mixed|null The value of the given variable or null when it is not found
*/
public function getEnv($name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
class EnvPlaceholderParameterBag extends ParameterBag
{
private $envPlaceholders = array();
private $envReferencedServices = array();

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

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

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

/**
* Returns the list of services referenced in `env()` parameters.
*
* @return string[] The list of services referenced in `env()` parameters
*/
public function getEnvReferencedServices()
{
return $this->envReferencedServices;
}

/**
* Merges the env placeholders of another EnvPlaceholderParameterBag.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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 Symfony\Component\DependencyInjection\Compiler\CheckEnvReferencedServicesPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\GetEnvInterface;

class CheckEnvReferencedServicePassTest extends \PHPUnit_Framework_TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$container->getParameterBag()->get('env(foo@a)');
$container->register('a', GetEnvService::class);

$pass = new CheckEnvReferencedServicesPass();
$pass->process($container);
}

/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
*/
public function testProcessThrowsExceptionOnInvalidReference()
{
$container = new ContainerBuilder();
$container->getParameterBag()->get('env(foo@a)');

$pass = new CheckEnvReferencedServicesPass();
$pass->process($container);
}

/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function testProcessThrowsExceptionOnInvalidReferenceFromInlinedDefinition()
{
$container = new ContainerBuilder();
$container->getParameterBag()->get('env(foo@a)');
$container->register('a', \stdClass::class);

$pass = new CheckEnvReferencedServicesPass();
$pass->process($container);
}
}

class GetEnvService implements GetEnvInterface
{
public function getEnv($name)
F438 {
return $name.$name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\GetEnvInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator;
Expand Down Expand Up @@ -344,7 +345,11 @@ public function testEnvParameter()
$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')), '->dump() dumps inline definitions which reference service_container');

require self::$fixturesPath.'/php/services26.php';
$container = new \Symfony_DI_PhpDumper_Test_EnvParameters();
$this->assertSame('BAZ123', $container->getParameter('baz'));
}

/**
Expand Down Expand Up @@ -671,3 +676,11 @@ public function testPrivateServiceTriggersDeprecation()
$container->get('bar');
}
}

class EnvResolver implements GetEnvInterface
{
public function getEnv($name)
{
return strtoupper($name).'123';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
namespace Symfony\Component\DependencyInjection\Tests\Dumper;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Dumper\YamlDumper;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Parser;

Expand Down Expand Up @@ -65,6 +67,16 @@ public function testDumpAutowireData()
$this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services24.yml', $dumper->dump());
}

public function testDumpLoad()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services_dump_load.yml');

$dumper = new YamlDumper($container);
$this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_dump_load.yml', $dumper->dump());
}

public function testInlineServices()
{
$container = new ContainerBuilder();
Expand Down
Loading
0