[DI] Rework config hierarchy: defaults > instanceof > service config

This commit is contained in:
Nicolas Grekas 2017-04-07 18:38:55 +02:00
parent cbaee55223
commit ab86457b12
8 changed files with 297 additions and 276 deletions

View File

@ -42,7 +42,8 @@ class PassConfig
$this->beforeOptimizationPasses = array(
100 => array(
$resolveClassPass = new ResolveClassPass(),
new ResolveDefinitionInheritancePass(),
new ResolveInstanceofConditionalsPass(),
new ResolveTagsInheritancePass(),
),
);

View File

@ -1,99 +0,0 @@
<?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\ChildDefinition;
use Symfony\Component\DependencyInjection\Definition;
/**
* Applies tags and instanceof inheritance to definitions.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ResolveDefinitionInheritancePass extends AbstractRecursivePass
{
protected function processValue($value, $isRoot = false)
{
if (!$value instanceof Definition) {
return parent::processValue($value, $isRoot);
}
$class = $value instanceof ChildDefinition ? $this->resolveDefinition($value) : $value->getClass();
if (!$class || false !== strpos($class, '%') || !$instanceof = $value->getInstanceofConditionals()) {
return parent::processValue($value, $isRoot);
}
$value->setInstanceofConditionals(array());
foreach ($instanceof as $interface => $definition) {
if ($interface !== $class && (!$this->container->getReflectionClass($interface) || !$this->container->getReflectionClass($class))) {
continue;
}
if ($interface === $class || is_subclass_of($class, $interface)) {
$this->mergeDefinition($value, $definition);
}
}
return parent::processValue($value, $isRoot);
}
/**
* Populates the class and tags from parent definitions.
*/
private function resolveDefinition(ChildDefinition $definition)
{
if (!$this->container->has($parent = $definition->getParent())) {
return;
}
$parentDef = $this->container->findDefinition($parent);
$class = $parentDef instanceof ChildDefinition ? $this->resolveDefinition($parentDef) : $parentDef->getClass();
$class = $definition->getClass() ?: $class;
// append parent tags when inheriting is enabled
if ($definition->getInheritTags()) {
$definition->setInheritTags(false);
foreach ($parentDef->getTags() as $k => $v) {
foreach ($v as $v) {
$definition->addTag($k, $v);
}
}
}
return $class;
}
private function mergeDefinition(Definition $def, ChildDefinition $definition)
{
$changes = $definition->getChanges();
if (isset($changes['shared'])) {
$def->setShared($definition->isShared());
}
if (isset($changes['abstract'])) {
$def->setAbstract($definition->isAbstract());
}
ResolveDefinitionTemplatesPass::mergeDefinition($def, $definition);
// prepend instanceof tags
$tailTags = $def->getTags();
if ($headTags = $definition->getTags()) {
$def->setTags($headTags);
foreach ($tailTags as $k => $v) {
foreach ($v as $v) {
$def->addTag($k, $v);
}
}
}
}
}

View File

@ -84,7 +84,7 @@ class ResolveDefinitionTemplatesPass extends AbstractRecursivePass
$def = new Definition();
// merge in parent definition
// purposely ignored attributes: abstract, tags
// purposely ignored attributes: abstract, shared, tags
$def->setClass($parentDef->getClass());
$def->setArguments($parentDef->getArguments());
$def->setMethodCalls($parentDef->getMethodCalls());
@ -101,27 +101,8 @@ class ResolveDefinitionTemplatesPass extends AbstractRecursivePass
$def->setPublic($parentDef->isPublic());
$def->setLazy($parentDef->isLazy());
$def->setAutowired($parentDef->isAutowired());
$def->setChanges($parentDef->getChanges());
self::mergeDefinition($def, $definition);
// merge autowiring types
foreach ($definition->getAutowiringTypes(false) as $autowiringType) {
$def->addAutowiringType($autowiringType);
}
// these attributes are always taken from the child
$def->setAbstract($definition->isAbstract());
$def->setShared($definition->isShared());
$def->setTags($definition->getTags());
return $def;
}
/**
* @internal
*/
public static function mergeDefinition(Definition $def, ChildDefinition $definition)
{
// overwrite with values specified in the decorator
$changes = $definition->getChanges();
if (isset($changes['class'])) {
@ -148,6 +129,9 @@ class ResolveDefinitionTemplatesPass extends AbstractRecursivePass
if (isset($changes['autowired'])) {
$def->setAutowired($definition->isAutowired());
}
if (isset($changes['shared'])) {
$def->setShared($definition->isShared());
}
if (isset($changes['decorated_service'])) {
$decoratedService = $definition->getDecoratedService();
if (null === $decoratedService) {
@ -182,5 +166,16 @@ class ResolveDefinitionTemplatesPass extends AbstractRecursivePass
if ($calls = $definition->getMethodCalls()) {
$def->setMethodCalls(array_merge($def->getMethodCalls(), $calls));
}
// merge autowiring types
foreach ($definition->getAutowiringTypes(false) as $autowiringType) {
$def->addAutowiringType($autowiringType);
}
// these attributes are always taken from the child
$def->setAbstract($definition->isAbstract());
$def->setTags($definition->getTags());
return $def;
}
}

View File

@ -0,0 +1,90 @@
<?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\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
/**
* Applies instanceof conditionals to definitions.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ResolveInstanceofConditionalsPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$didProcess = false;
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition instanceof ChildDefinition) {
continue;
}
if ($definition !== $processedDefinition = $this->processDefinition($container, $id, $definition)) {
$didProcess = true;
$container->setDefinition($id, $processedDefinition);
}
}
if ($didProcess) {
$container->register('abstract.'.__CLASS__, '')->setAbstract(true);
}
}
private function processDefinition(ContainerBuilder $container, $id, Definition $definition)
{
if (!$instanceofConditionals = $definition->getInstanceofConditionals()) {
return $definition;
}
if (!$class = $container->getParameterBag()->resolveValue($definition->getClass())) {
return $definition;
}
$definition->setInstanceofConditionals(array());
$instanceofParent = null;
$parent = 'abstract.'.__CLASS__;
$shared = null;
foreach ($instanceofConditionals as $interface => $instanceofDef) {
if ($interface !== $class && (!$container->getReflectionClass($interface) || !$container->getReflectionClass($class))) {
continue;
}
if ($interface === $class || is_subclass_of($class, $interface)) {
$instanceofParent = clone $instanceofDef;
$instanceofParent->setAbstract(true)->setInheritTags(true)->setParent($parent);
$parent = 'instanceof.'.$interface.'.'.$id;
$container->setDefinition($parent, $instanceofParent);
if (isset($instanceofParent->getChanges()['shared'])) {
$shared = $instanceofParent->isShared();
}
}
}
if ($instanceofParent) {
// cast Definition to ChildDefinition
$definition = serialize($definition);
$definition = substr_replace($definition, '53', 2, 2);
$definition = substr_replace($definition, 'Child', 44, 0);
$definition = unserialize($definition);
$definition->setInheritTags(true)->setParent($parent);
if (null !== $shared && !isset($definition->getChanges()['shared'])) {
$definition->setShared($shared);
}
}
return $definition;
}
}

View File

@ -0,0 +1,51 @@
<?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\ChildDefinition;
/**
* Applies tags inheritance to definitions.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ResolveTagsInheritancePass extends AbstractRecursivePass
{
/**
* {@inheritdoc}
*/
protected function processValue($value, $isRoot = false)
{
if (!$value instanceof ChildDefinition || !$value->getInheritTags()) {
return parent::processValue($value, $isRoot);
}
$value->setInheritTags(false);
if (!$this->container->has($parent = $value->getParent())) {
return parent::processValue($value, $isRoot);
}
$parentDef = $this->container->findDefinition($parent);
if ($parentDef instanceof ChildDefinition) {
$this->processValue($parentDef);
}
foreach ($parentDef->getTags() as $k => $v) {
foreach ($v as $v) {
$value->addTag($k, $v);
}
}
return parent::processValue($value, $isRoot);
}
}

View File

@ -1,155 +0,0 @@
<?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\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ResolveDefinitionInheritancePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ResolveDefinitionInheritancePassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$def = $container->register('parent', self::class)->setArguments(array('moo', 'b'))->setProperty('foo', 'moo');
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))
->replaceArgument(0, 'a')
->setProperty('foo', 'bar')
->setClass('bar'),
));
$this->process($container);
$this->assertEmpty($def->getInstanceofConditionals());
$this->assertSame($def, $container->getDefinition('parent'));
$this->assertEquals('bar', $def->getClass());
$this->assertEquals(array('a', 'b'), $def->getArguments());
$this->assertEquals(array('foo' => 'bar'), $def->getProperties());
}
public function testProcessAppendsMethodCallsAlways()
{
$container = new ContainerBuilder();
$def = $container
->register('parent', self::class)
->addMethodCall('foo', array('bar'));
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))
->addMethodCall('bar', array('foo')),
));
$this->process($container);
$this->assertEquals(array(
array('foo', array('bar')),
array('bar', array('foo')),
), $container->getDefinition('parent')->getMethodCalls());
}
public function testProcessDoesReplaceShared()
{
$container = new ContainerBuilder();
$def = $container->register('parent', 'stdClass');
$def->setInstanceofConditionals(array(
'stdClass' => (new ChildDefinition(''))->setShared(false),
));
$this->process($container);
$this->assertFalse($def->isShared());
}
public function testProcessHandlesMultipleInheritance()
{
$container = new ContainerBuilder();
$def = $container
->register('parent', self::class)
->setArguments(array('foo', 'bar', 'c'))
;
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))->replaceArgument(1, 'b'),
self::class => (new ChildDefinition(''))->replaceArgument(0, 'a'),
));
$this->process($container);
$this->assertEquals(array('a', 'b', 'c'), $def->getArguments());
}
public function testSetLazyOnServiceHasParent()
{
$container = new ContainerBuilder();
$def = $container->register('parent', 'stdClass');
$def->setInstanceofConditionals(array(
'stdClass' => (new ChildDefinition(''))->setLazy(true),
));
$this->process($container);
$this->assertTrue($container->getDefinition('parent')->isLazy());
}
public function testProcessInheritTags()
{
$container = new ContainerBuilder();
$container->register('parent', self::class)->addTag('parent');
$def = $container->setDefinition('child', new ChildDefinition('parent'))
->addTag('child')
->setInheritTags(true)
;
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))->addTag('foo'),
));
$this->process($container);
$t = array(array());
$this->assertSame(array('foo' => $t, 'child' => $t, 'parent' => $t), $def->getTags());
}
public function testProcessResolvesAliasesAndTags()
{
$container = new ContainerBuilder();
$container->register('parent', self::class);
$container->setAlias('parent_alias', 'parent');
$def = $container->setDefinition('child', new ChildDefinition('parent_alias'));
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))->addTag('foo'),
));
$this->process($container);
$this->assertSame(array('foo' => array(array())), $def->getTags());
$this->assertSame($def, $container->getDefinition('child'));
$this->assertEmpty($def->getClass());
}
protected function process(ContainerBuilder $container)
{
$pass = new ResolveDefinitionInheritancePass();
$pass->process($container);
}
}

View File

@ -0,0 +1,104 @@
<?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\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveDefinitionTemplatesPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ResolveInstanceofConditionalsPassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$def = $container->register('foo', self::class);
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))->setProperty('foo', 'bar'),
));
(new ResolveInstanceofConditionalsPass())->process($container);
$parent = 'instanceof.'.parent::class.'.foo';
$def = $container->getDefinition('foo');
$this->assertEmpty($def->getInstanceofConditionals());
$this->assertInstanceof(ChildDefinition::class, $def);
$this->assertTrue($def->getInheritTags());
$this->assertSame($parent, $def->getParent());
$this->assertEquals(array('foo' => 'bar'), $container->getDefinition($parent)->getProperties());
}
public function testProcessInheritance()
{
$container = new ContainerBuilder();
$def = $container
->register('parent', parent::class)
->addMethodCall('foo', array('foo'));
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))->addMethodCall('foo', array('bar')),
));
$def = (new ChildDefinition('parent'))->setClass(self::class);
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))->addMethodCall('foo', array('baz')),
));
$container->setDefinition('child', $def);
(new ResolveInstanceofConditionalsPass())->process($container);
(new ResolveDefinitionTemplatesPass())->process($container);
$expected = array(
array('foo', array('bar')),
array('foo', array('foo')),
);
$this->assertSame($expected, $container->getDefinition('parent')->getMethodCalls());
$this->assertSame($expected, $container->getDefinition('child')->getMethodCalls());
}
public function testProcessDoesReplaceShared()
{
$container = new ContainerBuilder();
$def = $container->register('foo', 'stdClass');
$def->setInstanceofConditionals(array(
'stdClass' => (new ChildDefinition(''))->setShared(false),
));
(new ResolveInstanceofConditionalsPass())->process($container);
$def = $container->getDefinition('foo');
$this->assertFalse($def->isShared());
}
public function testProcessHandlesMultipleInheritance()
{
$container = new ContainerBuilder();
$def = $container->register('foo', self::class)->setShared(true);
$def->setInstanceofConditionals(array(
parent::class => (new ChildDefinition(''))->setLazy(true)->setShared(false),
self::class => (new ChildDefinition(''))->setAutowired(true),
));
(new ResolveInstanceofConditionalsPass())->process($container);
(new ResolveDefinitionTemplatesPass())->process($container);
$def = $container->getDefinition('foo');
$this->assertTrue($def->isAutowired());
$this->assertTrue($def->isLazy());
$this->assertTrue($def->isShared());
}
}

View File

@ -0,0 +1,34 @@
<?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\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ResolveTagsInheritancePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ResolveTagsInheritancePassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$container->register('grandpa', self::class)->addTag('g');
$container->setDefinition('parent', new ChildDefinition('grandpa'))->addTag('p')->setInheritTags(true);
$container->setDefinition('child', new ChildDefinition('parent'))->setInheritTags(true);
(new ResolveTagsInheritancePass())->process($container);
$expected = array('p' => array(array()), 'g' => array(array()));
$this->assertSame($expected, $container->getDefinition('parent')->getTags());
$this->assertSame($expected, $container->getDefinition('child')->getTags());
}
}