feature #22356 [DI] Rework config hierarchy: defaults > instanceof > service config (weaverryan, nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

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

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

Replaces #22294.
The main change is that ChildDefinition does not have "instanceof" applied anymore. All the complexity of the pass came from the merging logic required to deal with them. But in fact, we'd better not have any such logic and just not apply "instanceof" to ChildDefinition (but have them inherit from their parents with the usual logic).

Commits
-------

6d6116b920 Adding an integration test for the hirarchy of defaults, instanceof, child, parent definitions
ab86457b12 [DI] Rework config hierarchy: defaults > instanceof > service config
cbaee55223 [DI] Track changes at the "Definition" level
This commit is contained in:
Fabien Potencier 2017-04-11 09:32:21 -07:00
commit 9950b90bb6
22 changed files with 575 additions and 442 deletions

View File

@ -23,15 +23,12 @@ class ChildDefinition extends Definition
{
private $parent;
private $inheritTags = false;
private $changes = array();
/**
* @param string $parent The id of Definition instance to decorate
*/
public function __construct($parent)
{
parent::__construct();
$this->parent = $parent;
}
@ -46,13 +43,17 @@ class ChildDefinition extends Definition
}
/**
* Returns all changes tracked for the Definition object.
* Sets the Definition being decorated.
*
* @return array An array of changes for this Definition
* @param string $parent
*
* @return $this
*/
public function getChanges()
public function setParent($parent)
{
return $this->changes;
$this->parent = $parent;
return $this;
}
/**
@ -79,116 +80,6 @@ class ChildDefinition extends Definition
return $this->inheritTags;
}
/**
* {@inheritdoc}
*/
public function setClass($class)
{
$this->changes['class'] = true;
return parent::setClass($class);
}
/**
* {@inheritdoc}
*/
public function setFactory($callable)
{
$this->changes['factory'] = true;
return parent::setFactory($callable);
}
/**
* {@inheritdoc}
*/
public function setConfigurator($callable)
{
$this->changes['configurator'] = true;
return parent::setConfigurator($callable);
}
/**
* {@inheritdoc}
*/
public function setFile($file)
{
$this->changes['file'] = true;
return parent::setFile($file);
}
/**
* {@inheritdoc}
*/
public function setShared($boolean)
{
$this->changes['shared'] = true;
return parent::setShared($boolean);
}
/**
* {@inheritdoc}
*/
public function setPublic($boolean)
{
$this->changes['public'] = true;
return parent::setPublic($boolean);
}
/**
* {@inheritdoc}
*/
public function setLazy($boolean)
{
$this->changes['lazy'] = true;
return parent::setLazy($boolean);
}
/**
* {@inheritdoc}
*/
public function setAbstract($boolean)
{
$this->changes['abstract'] = true;
return parent::setAbstract($boolean);
}
/**
* {@inheritdoc}
*/
public function setDecoratedService($id, $renamedId = null, $priority = 0)
{
$this->changes['decorated_service'] = true;
return parent::setDecoratedService($id, $renamedId, $priority);
}
/**
* {@inheritdoc}
*/
public function setDeprecated($boolean = true, $template = null)
{
$this->changes['deprecated'] = true;
return parent::setDeprecated($boolean, $template);
}
/**
* {@inheritdoc}
*/
public function setAutowired($autowired)
{
$this->changes['autowired'] = true;
return parent::setAutowired($autowired);
}
/**
* Gets an argument to pass to the service constructor/factory method.
*

View File

@ -65,11 +65,12 @@ abstract class AbstractRecursivePass implements CompilerPassInterface
$value->setProperties($this->processValue($value->getProperties()));
$value->setMethodCalls($this->processValue($value->getMethodCalls()));
if ($v = $value->getFactory()) {
$value->setFactory($this->processValue($v));
$changes = $value->getChanges();
if (isset($changes['factory'])) {
$value->setFactory($this->processValue($value->getFactory()));
}
if ($v = $value->getConfigurator()) {
$value->setConfigurator($this->processValue($v));
if (isset($changes['configurator'])) {
$value->setConfigurator($this->processValue($value->getConfigurator()));
}
}

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) {
@ -177,5 +161,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,91 @@
<?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) {
// don't apply "instanceof" to children: it will be applied to their parent
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

@ -58,8 +58,13 @@ class ResolveParameterPlaceHoldersPass extends AbstractRecursivePass
return $this->bag->resolveValue($value);
}
if ($value instanceof Definition) {
$value->setClass($this->bag->resolveValue($value->getClass()));
$value->setFile($this->bag->resolveValue($value->getFile()));
$changes = $value->getChanges();
if (isset($changes['class'])) {
$value->setClass($this->bag->resolveValue($value->getClass()));
}
if (isset($changes['file'])) {
$value->setFile($this->bag->resolveValue($value->getFile()));
}
$value->setProperties($this->bag->resolveValue($value->getProperties()));
$value->setMethodCalls($this->bag->resolveValue($value->getMethodCalls()));
}

View File

@ -43,7 +43,9 @@ class ResolveReferencesToAliasesPass implements CompilerPassInterface
$definition->setArguments($this->processArguments($definition->getArguments()));
$definition->setMethodCalls($this->processArguments($definition->getMethodCalls()));
$definition->setProperties($this->processArguments($definition->getProperties()));
$definition->setFactory($this->processFactory($definition->getFactory()));
if (isset($definition->getChanges()['factory'])) {
$definition->setFactory($this->processFactory($definition->getFactory()));
}
}
foreach ($container->getAliases() as $id => $alias) {

View File

@ -0,0 +1,52 @@
<?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\Exception\RuntimeException;
/**
* 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())) {
throw new RuntimeException(sprintf('Parent definition "%s" does not exist.', $parent));
}
$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

@ -39,8 +39,9 @@ class Definition
private $decoratedService;
private $autowired = false;
private $autowiringTypes = array();
private $changes = array();
protected $arguments;
protected $arguments = array();
/**
* @param string|null $class The service class
@ -48,10 +49,34 @@ class Definition
*/
public function __construct($class = null, array $arguments = array())
{
$this->class = $class;
if (null !== $class) {
$this->setClass($class);
}
$this->arguments = $arguments;
}
/**
* Returns all changes tracked for the Definition object.
*
* @return array An array of changes for this Definition
*/
public function getChanges()
{
return $this->changes;
}
/**
* Sets the tracked changes for the Definition object.
*
* @return $this
*/
public function setChanges(array $changes)
{
$this->changes = $changes;
return $this;
}
/**
* Sets a factory.
*
@ -61,6 +86,8 @@ class Definition
*/
public function setFactory($factory)
{
$this->changes['factory'] = true;
if (is_string($factory) && strpos($factory, '::') !== false) {
$factory = explode('::', $factory, 2);
}
@ -97,6 +124,8 @@ class Definition
throw new InvalidArgumentException(sprintf('The decorated service inner name for "%s" must be different than the service name itself.', $id));
}
$this->changes['decorated_service'] = true;
if (null === $id) {
$this->decoratedService = null;
} else {
@ -125,6 +154,8 @@ class Definition
*/
public function setClass($class)
{
$this->changes['class'] = true;
$this->class = $class;
return $this;
@ -455,6 +486,8 @@ class Definition
*/
public function setFile($file)
{
$this->changes['file'] = true;
$this->file = $file;
return $this;
@ -479,6 +512,8 @@ class Definition
*/
public function setShared($shared)
{
$this->changes['shared'] = true;
$this->shared = (bool) $shared;
return $this;
@ -503,6 +538,8 @@ class Definition
*/
public function setPublic($boolean)
{
$this->changes['public'] = true;
$this->public = (bool) $boolean;
return $this;
@ -527,6 +564,8 @@ class Definition
*/
public function setLazy($lazy)
{
$this->changes['lazy'] = true;
$this->lazy = (bool) $lazy;
return $this;
@ -619,6 +658,8 @@ class Definition
$this->deprecationTemplate = $template;
}
$this->changes['deprecated'] = true;
$this->deprecated = (bool) $status;
return $this;
@ -656,6 +697,8 @@ class Definition
*/
public function setConfigurator($configurator)
{
$this->changes['configurator'] = true;
if (is_string($configurator) && strpos($configurator, '::') !== false) {
$configurator = explode('::', $configurator, 2);
}
@ -716,6 +759,8 @@ class Definition
*/
public function setAutowired($autowired)
{
$this->changes['autowired'] = true;
$this->autowired = (bool) $autowired;
return $this;

View File

@ -222,12 +222,19 @@ class XmlFileLoader extends FileLoader
$defaults = array();
} else {
$definition = new Definition();
if (isset($defaults['public'])) {
$definition->setPublic($defaults['public']);
}
if (isset($defaults['autowire'])) {
$definition->setAutowired($defaults['autowire']);
}
$definition->setChanges(array());
}
if ($publicAttr = $service->getAttribute('public')) {
$definition->setPublic(XmlUtils::phpize($publicAttr));
} elseif (isset($defaults['public'])) {
$definition->setPublic($defaults['public']);
}
foreach (array('class', 'shared', 'synthetic', 'lazy', 'abstract') as $key) {
@ -239,8 +246,6 @@ class XmlFileLoader extends FileLoader
if ($value = $service->getAttribute('autowire')) {
$definition->setAutowired(XmlUtils::phpize($value));
} elseif (isset($defaults['autowire'])) {
$definition->setAutowired($defaults['autowire']);
}
if ($files = $this->getChildren($service, 'file')) {

View File

@ -81,10 +81,6 @@ class YamlFileLoader extends FileLoader
'shared' => 'shared',
'lazy' => 'lazy',
'public' => 'public',
'abstract' => 'abstract',
'deprecated' => 'deprecated',
'factory' => 'factory',
'arguments' => 'arguments',
'properties' => 'properties',
'configurator' => 'configurator',
'calls' => 'calls',
@ -366,6 +362,15 @@ class YamlFileLoader extends FileLoader
$defaults = array();
} else {
$definition = new Definition();
if (isset($defaults['public'])) {
$definition->setPublic($defaults['public']);
}
if (isset($defaults['autowire'])) {
$definition->setAutowired($defaults['autowire']);
}
$definition->setChanges(array());
}
if (isset($service['class'])) {
@ -384,9 +389,8 @@ class YamlFileLoader extends FileLoader
$definition->setLazy($service['lazy']);
}
$public = isset($service['public']) ? $service['public'] : (isset($defaults['public']) ? $defaults['public'] : null);
if (null !== $public) {
$definition->setPublic($public);
if (isset($service['public'])) {
$definition->setPublic($service['public']);
}
if (isset($service['abstract'])) {
@ -484,9 +488,8 @@ class YamlFileLoader extends FileLoader
$definition->setDecoratedService($service['decorates'], $renameId, $priority);
}
$autowire = isset($service['autowire']) ? $service['autowire'] : (isset($defaults['autowire']) ? $defaults['autowire'] : null);
if (null !== $autowire) {
$definition->setAutowired($autowire);
if (isset($service['autowire'])) {
$definition->setAutowired($service['autowire']);
}
if (isset($service['autowiring_types'])) {

View File

@ -136,10 +136,7 @@
<xsd:complexType name="instanceof">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="configurator" type="callable" minOccurs="0" maxOccurs="1" />
<xsd:element name="factory" type="callable" minOccurs="0" maxOccurs="1" />
<xsd:element name="deprecated" type="xsd:string" minOccurs="0" maxOccurs="1" />
<xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
@ -148,7 +145,6 @@
<xsd:attribute name="shared" type="boolean" />
<xsd:attribute name="public" type="boolean" />
<xsd:attribute name="lazy" type="boolean" />
<xsd:attribute name="abstract" type="boolean" />
<xsd:attribute name="autowire" type="boolean" />
</xsd:complexType>

View File

@ -12,7 +12,9 @@
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -114,4 +116,79 @@ class IntegrationTest extends TestCase
$this->assertFalse($container->hasDefinition('b'));
$this->assertFalse($container->hasDefinition('c'), 'Service C was not inlined.');
}
public function testInstanceofDefaultsAndParentDefinitionResolution()
{
$container = new ContainerBuilder();
$container->setResourceTracking(false);
// loading YAML with an expressive test-case in that file
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Fixtures/yaml'));
$loader->load('services_defaults_instanceof_parent.yml');
$container->compile();
// instanceof overrides defaults
$simpleService = $container->getDefinition('service_simple');
$this->assertFalse($simpleService->isAutowired());
$this->assertFalse($simpleService->isShared());
// all tags are kept
$this->assertEquals(
array(
'foo_tag' => array(array('priority' => 100), array()),
'bar_tag' => array(array()),
),
$simpleService->getTags()
);
// calls are all kept, but service-level calls are last
$this->assertEquals(
array(
// from instanceof
array('setSunshine', array('bright')),
// from service
array('enableSummer', array(true)),
array('setSunshine', array('warm')),
),
$simpleService->getMethodCalls()
);
// service override instanceof
$overrideService = $container->getDefinition('service_override_instanceof');
$this->assertTrue($overrideService->isAutowired());
// children definitions get no instanceof
$childDef = $container->getDefinition('child_service');
$this->assertEmpty($childDef->getTags());
$childDef2 = $container->getDefinition('child_service_with_parent_instanceof');
// taken from instanceof applied to parent
$this->assertFalse($childDef2->isAutowired());
// override the instanceof
$this->assertTrue($childDef2->isShared());
// tags inherit like normal
$this->assertEquals(
array(
'foo_tag' => array(array('priority' => 100), array()),
'bar_tag' => array(array()),
),
$simpleService->getTags()
);
}
}
class IntegrationTestStub extends IntegrationTestStubParent
{
}
class IntegrationTestStubParent
{
public function enableSummer($enable)
{
// methods used in calls - added here to prevent errors for not existing
}
public function setSunshine($type)
{
}
}

View File

@ -1,170 +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 testProcessDoesReplaceAbstract()
{
$container = new ContainerBuilder();
$def = $container->register('parent', 'stdClass');
$def->setInstanceofConditionals(array(
'stdClass' => (new ChildDefinition(''))->setAbstract(true),
));
$this->process($container);
$this->assertTrue($def->isAbstract());
}
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());
}
}

View File

@ -20,6 +20,7 @@ class DefinitionTest extends TestCase
{
$def = new Definition('stdClass');
$this->assertEquals('stdClass', $def->getClass(), '__construct() takes the class name as its first argument');
$this->assertSame(array('class' => true), $def->getChanges());
$def = new Definition('stdClass', array('foo'));
$this->assertEquals(array('foo'), $def->getArguments(), '__construct() takes an optional array of arguments as its second argument');
@ -27,13 +28,14 @@ class DefinitionTest extends TestCase
public function testSetGetFactory()
{
$def = new Definition('stdClass');
$def = new Definition();
$this->assertSame($def, $def->setFactory('foo'), '->setFactory() implements a fluent interface');
$this->assertEquals('foo', $def->getFactory(), '->getFactory() returns the factory');
$def->setFactory('Foo::bar');
$this->assertEquals(array('Foo', 'bar'), $def->getFactory(), '->setFactory() converts string static method call to the array');
$this->assertSame(array('factory' => true), $def->getChanges());
}
public function testSetGetClass()
@ -315,6 +317,51 @@ class DefinitionTest extends TestCase
$this->assertFalse($def->isAutowired());
}
public function testChangesNoChanges()
{
$def = new Definition();
$this->assertSame(array(), $def->getChanges());
}
public function testGetChangesWithChanges()
{
$def = new Definition('stdClass', array('fooarg'));
$def->setAbstract(true);
$def->setAutowired(true);
$def->setConfigurator('configuration_func');
$def->setDecoratedService(null);
$def->setDeprecated(true);
$def->setFactory('factory_func');
$def->setFile('foo.php');
$def->setLazy(true);
$def->setPublic(true);
$def->setShared(true);
$def->setSynthetic(true);
// changes aren't tracked for these, class or arguments
$def->setInstanceofConditionals(array());
$def->addTag('foo_tag');
$def->addMethodCall('methodCall');
$def->setProperty('fooprop', true);
$this->assertSame(array(
'class' => true,
'autowired' => true,
'configurator' => true,
'decorated_service' => true,
'deprecated' => true,
'factory' => true,
'file' => true,
'lazy' => true,
'public' => true,
'shared' => true,
), $def->getChanges());
$def->setChanges(array());
$this->assertSame(array(), $def->getChanges());
}
/**
* @group legacy
*/

View File

@ -5,7 +5,8 @@ services:
autowire: true
DummyInterface:
arguments: [ !service { class: Anonymous } ]
properties:
foo: !service { class: Anonymous }
# Ensure next conditionals are not considered as services
Bar:

View File

@ -0,0 +1,49 @@
services:
_defaults:
autowire: true
_instanceof:
Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStubParent:
# should override _defaults
autowire: false
shared: false
tags:
- { name: foo_tag }
calls:
- [setSunshine, [bright]]
# a second instanceof that will be applied
Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub:
tags:
- { name: bar_tag }
service_simple:
class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub
tags:
- { name: foo_tag, priority: 100 }
# calls from instanceof are kept, but this comes later
calls:
- [enableSummer, [true]]
- [setSunshine, [warm]]
service_override_instanceof:
class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub
# override instanceof
autowire: true
parent_service:
abstract: true
lazy: true
# instanceof will not be applied to this
child_service:
class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub
parent: parent_service
parent_service_with_class:
abstract: true
class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub
child_service_with_parent_instanceof:
parent: parent_service_with_class
shared: true

View File

@ -648,6 +648,8 @@ class XmlFileLoaderTest extends TestCase
$this->assertFalse($container->getDefinition('with_defaults')->isPublic());
$this->assertSame(array('foo' => array(array())), $container->getDefinition('with_defaults')->getTags());
$this->assertTrue($container->getDefinition('with_defaults')->isAutowired());
$this->assertArrayNotHasKey('public', $container->getDefinition('with_defaults')->getChanges());
$this->assertArrayNotHasKey('autowire', $container->getDefinition('with_defaults')->getChanges());
$this->assertArrayNotHasKey('public', $container->getDefinition('no_defaults_child')->getChanges());
$this->assertArrayNotHasKey('autowire', $container->getDefinition('no_defaults_child')->getChanges());

View File

@ -24,7 +24,6 @@ use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
use Symfony\Component\ExpressionLanguage\Expression;
@ -398,6 +397,8 @@ class YamlFileLoaderTest extends TestCase
$this->assertFalse($container->getDefinition('with_defaults')->isPublic());
$this->assertSame(array('foo' => array(array())), $container->getDefinition('with_defaults')->getTags());
$this->assertTrue($container->getDefinition('with_defaults')->isAutowired());
$this->assertArrayNotHasKey('public', $container->getDefinition('with_defaults')->getChanges());
$this->assertArrayNotHasKey('autowire', $container->getDefinition('with_defaults')->getChanges());
$this->assertFalse($container->getAlias('with_defaults_aliased')->isPublic());
$this->assertFalse($container->getAlias('with_defaults_aliased_short')->isPublic());
@ -532,12 +533,12 @@ class YamlFileLoaderTest extends TestCase
$this->assertCount(3, $instanceof);
$this->assertArrayHasKey('DummyInterface', $instanceof);
$args = $instanceof['DummyInterface']->getArguments();
$args = $instanceof['DummyInterface']->getProperties();
$this->assertCount(1, $args);
$this->assertInstanceOf(Reference::class, $args[0]);
$this->assertTrue($container->has((string) $args[0]));
$this->assertInstanceOf(Reference::class, $args['foo']);
$this->assertTrue($container->has((string) $args['foo']));
$anonymous = $container->getDefinition((string) $args[0]);
$anonymous = $container->getDefinition((string) $args['foo']);
$this->assertEquals('Anonymous', $anonymous->getClass());
$this->assertFalse($anonymous->isPublic());
$this->assertEmpty($anonymous->getInstanceofConditionals());