diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 5d1e803ab3..e4ef2b291d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -38,6 +38,7 @@ class UnusedTagsPass implements CompilerPassInterface 'container.service_locator', 'container.service_locator_context', 'container.service_subscriber', + 'container.stack', 'controller.argument_value_resolver', 'controller.service_arguments', 'data_collector', diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 4586355055..03d4a57d52 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -51,6 +51,7 @@ class PassConfig $this->optimizationPasses = [[ new AutoAliasServicePass(), new ValidateEnvPlaceholdersPass(), + new ResolveDecoratorStackPass(), new ResolveChildDefinitionsPass(), new RegisterServiceSubscribersPass(), new ResolveParameterPlaceHoldersPass(false, false), diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDecoratorStackPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDecoratorStackPass.php new file mode 100644 index 0000000000..61202adf33 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDecoratorStackPass.php @@ -0,0 +1,127 @@ + + * + * 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\Alias; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Nicolas Grekas + */ +class ResolveDecoratorStackPass implements CompilerPassInterface +{ + private $tag; + + public function __construct(string $tag = 'container.stack') + { + $this->tag = $tag; + } + + public function process(ContainerBuilder $container) + { + $stacks = []; + + foreach ($container->findTaggedServiceIds($this->tag) as $id => $tags) { + $definition = $container->getDefinition($id); + + if (!$definition instanceof ChildDefinition) { + throw new InvalidArgumentException(sprintf('Invalid service "%s": only definitions with a "parent" can have the "%s" tag.', $id, $this->tag)); + } + + if (!$stack = $definition->getArguments()) { + throw new InvalidArgumentException(sprintf('Invalid service "%s": the stack of decorators is empty.', $id)); + } + + $stacks[$id] = $stack; + } + + if (!$stacks) { + return; + } + + $resolvedDefinitions = []; + + foreach ($container->getDefinitions() as $id => $definition) { + if (!isset($stacks[$id])) { + $resolvedDefinitions[$id] = $definition; + continue; + } + + foreach (array_reverse($this->resolveStack($stacks, [$id]), true) as $k => $v) { + $resolvedDefinitions[$k] = $v; + } + + $alias = $container->setAlias($id, $k); + + if ($definition->getChanges()['public'] ?? false) { + $alias->setPublic($definition->isPublic()); + } + + if ($definition->isDeprecated()) { + $alias->setDeprecated(...array_values($definition->getDeprecation('%alias_id%'))); + } + } + + $container->setDefinitions($resolvedDefinitions); + } + + private function resolveStack(array $stacks, array $path): array + { + $definitions = []; + $id = end($path); + $prefix = '.'.$id.'.'; + + if (!isset($stacks[$id])) { + return [$id => new ChildDefinition($id)]; + } + + if (key($path) !== $searchKey = array_search($id, $path)) { + throw new ServiceCircularReferenceException($id, \array_slice($path, $searchKey)); + } + + foreach ($stacks[$id] as $k => $definition) { + if ($definition instanceof ChildDefinition && isset($stacks[$definition->getParent()])) { + $path[] = $definition->getParent(); + $definition = unserialize(serialize($definition)); // deep clone + } elseif ($definition instanceof Definition) { + $definitions[$decoratedId = $prefix.$k] = $definition; + continue; + } elseif ($definition instanceof Reference || $definition instanceof Alias) { + $path[] = (string) $definition; + } else { + throw new InvalidArgumentException(sprintf('Invalid service "%s": unexpected value of type "%s" found in the stack of decorators.', $id, get_debug_type($definition))); + } + + $p = $prefix.$k; + + foreach ($this->resolveStack($stacks, $path) as $k => $v) { + $definitions[$decoratedId = $p.$k] = $definition instanceof ChildDefinition ? $definition->setParent($k) : new ChildDefinition($k); + $definition = null; + } + array_pop($path); + } + + if (1 === \count($path)) { + foreach ($definitions as $k => $definition) { + $definition->setPublic(false)->setTags([])->setDecoratedService($decoratedId); + } + $definition->setDecoratedService(null); + } + + return $definitions; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractServiceConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractServiceConfigurator.php index 2257edaef6..68b3cb5e94 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractServiceConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractServiceConfigurator.php @@ -81,6 +81,18 @@ abstract class AbstractServiceConfigurator extends AbstractConfigurator return $this->parent->get($id); } + /** + * Registers a stack of decorator services. + * + * @param InlineServiceConfigurator[]|ReferenceConfigurator[] $services + */ + final public function stack(string $id, array $services): AliasConfigurator + { + $this->__destruct(); + + return $this->parent->stack($id, $services); + } + /** * Registers a service. */ diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php index a5e0084226..42efb181dc 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; @@ -131,6 +132,39 @@ class ServicesConfigurator extends AbstractConfigurator return new ServiceConfigurator($this->container, $definition->getInstanceofConditionals(), true, $this, $definition, $id, []); } + /** + * Registers a stack of decorator services. + * + * @param InlineServiceConfigurator[]|ReferenceConfigurator[] $services + */ + final public function stack(string $id, array $services): AliasConfigurator + { + foreach ($services as $i => $service) { + if ($service instanceof InlineServiceConfigurator) { + $definition = $service->definition->setInstanceofConditionals($this->instanceof); + + $changes = $definition->getChanges(); + $definition->setAutowired((isset($changes['autowired']) ? $definition : $this->defaults)->isAutowired()); + $definition->setAutoconfigured((isset($changes['autoconfigured']) ? $definition : $this->defaults)->isAutoconfigured()); + $definition->setBindings(array_merge($this->defaults->getBindings(), $definition->getBindings())); + $definition->setChanges($changes); + + $services[$i] = $definition; + } elseif (!$service instanceof ReferenceConfigurator) { + throw new InvalidArgumentException(sprintf('"%s()" expects a list of definitions as returned by "%s()" or "%s()", "%s" given at index "%s" for service "%s".', __METHOD__, InlineServiceConfigurator::FACTORY, ReferenceConfigurator::FACTORY, $service instanceof AbstractConfigurator ? $service::FACTORY.'()' : get_debug_type($service)), $i, $id); + } + } + + $alias = $this->alias($id, ''); + $alias->definition = $this->set($id) + ->parent('') + ->args($services) + ->tag('container.stack') + ->definition; + + return $alias; + } + /** * Registers a service. */ diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index f4c90f6f78..26928f2f3c 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -112,12 +112,12 @@ class XmlFileLoader extends FileLoader } } - private function parseDefinitions(\DOMDocument $xml, string $file, array $defaults) + private function parseDefinitions(\DOMDocument $xml, string $file, Definition $defaults) { $xpath = new \DOMXPath($xml); $xpath->registerNamespace('container', self::NS); - if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) { + if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype|//container:services/container:stack')) { return; } $this->setCurrentDir(\dirname($file)); @@ -126,12 +126,34 @@ class XmlFileLoader extends FileLoader $this->isLoadingInstanceof = true; $instanceof = $xpath->query('//container:services/container:instanceof'); foreach ($instanceof as $service) { - $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, [])); + $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, new Definition())); } $this->isLoadingInstanceof = false; foreach ($services as $service) { - if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) { + if ('stack' === $service->tagName) { + $service->setAttribute('parent', '-'); + $definition = $this->parseDefinition($service, $file, $defaults) + ->setTags(array_merge_recursive(['container.stack' => [[]]], $defaults->getTags())) + ; + $this->setDefinition($id = (string) $service->getAttribute('id'), $definition); + $stack = []; + + foreach ($this->getChildren($service, 'service') as $k => $frame) { + $k = $frame->getAttribute('id') ?: $k; + $frame->setAttribute('id', $id.'" at index "'.$k); + + if ($alias = $frame->getAttribute('alias')) { + $this->validateAlias($frame, $file); + $stack[$k] = new Reference($alias); + } else { + $stack[$k] = $this->parseDefinition($frame, $file, $defaults) + ->setInstanceofConditionals($this->instanceof); + } + } + + $definition->setArguments($stack); + } elseif (null !== $definition = $this->parseDefinition($service, $file, $defaults)) { if ('prototype' === $service->tagName) { $excludes = array_column($this->getChildren($service, 'exclude'), 'nodeValue'); if ($service->hasAttribute('exclude')) { @@ -148,51 +170,24 @@ class XmlFileLoader extends FileLoader } } - /** - * Get service defaults. - */ - private function getServiceDefaults(\DOMDocument $xml, string $file): array + private function getServiceDefaults(\DOMDocument $xml, string $file): Definition { $xpath = new \DOMXPath($xml); $xpath->registerNamespace('container', self::NS); if (null === $defaultsNode = $xpath->query('//container:services/container:defaults')->item(0)) { - return []; + return new Definition(); } - $bindings = []; - foreach ($this->getArgumentsAsPhp($defaultsNode, 'bind', $file) as $argument => $value) { - $bindings[$argument] = new BoundArgument($value, true, BoundArgument::DEFAULTS_BINDING, $file); - } + $defaultsNode->setAttribute('id', ''); - $defaults = [ - 'tags' => $this->getChildren($defaultsNode, 'tag'), - 'bind' => $bindings, - ]; - - foreach ($defaults['tags'] as $tag) { - if ('' === $tag->getAttribute('name')) { - throw new InvalidArgumentException(sprintf('The tag name for tag "" in "%s" must be a non-empty string.', $file)); - } - } - - if ($defaultsNode->hasAttribute('autowire')) { - $defaults['autowire'] = XmlUtils::phpize($defaultsNode->getAttribute('autowire')); - } - if ($defaultsNode->hasAttribute('public')) { - $defaults['public'] = XmlUtils::phpize($defaultsNode->getAttribute('public')); - } - if ($defaultsNode->hasAttribute('autoconfigure')) { - $defaults['autoconfigure'] = XmlUtils::phpize($defaultsNode->getAttribute('autoconfigure')); - } - - return $defaults; + return $this->parseDefinition($defaultsNode, $file, new Definition()); } /** * Parses an individual Definition. */ - private function parseDefinition(\DOMElement $service, string $file, array $defaults): ?Definition + private function parseDefinition(\DOMElement $service, string $file, Definition $defaults): ?Definition { if ($alias = $service->getAttribute('alias')) { $this->validateAlias($service, $file); @@ -200,8 +195,8 @@ class XmlFileLoader extends FileLoader $this->container->setAlias((string) $service->getAttribute('id'), $alias = new Alias($alias)); if ($publicAttr = $service->getAttribute('public')) { $alias->setPublic(XmlUtils::phpize($publicAttr)); - } elseif (isset($defaults['public'])) { - $alias->setPublic($defaults['public']); + } elseif ($defaults->getChanges()['public'] ?? false) { + $alias->setPublic($defaults->isPublic()); } if ($deprecated = $this->getChildren($service, 'deprecated')) { @@ -231,16 +226,11 @@ class XmlFileLoader extends FileLoader $definition = new Definition(); } - if (isset($defaults['public'])) { - $definition->setPublic($defaults['public']); + if ($defaults->getChanges()['public'] ?? false) { + $definition->setPublic($defaults->isPublic()); } - if (isset($defaults['autowire'])) { - $definition->setAutowired($defaults['autowire']); - } - if (isset($defaults['autoconfigure'])) { - $definition->setAutoconfigured($defaults['autoconfigure']); - } - + $definition->setAutowired($defaults->isAutowired()); + $definition->setAutoconfigured($defaults->isAutoconfigured()); $definition->setChanges([]); foreach (['class', 'public', 'shared', 'synthetic', 'abstract'] as $key) { @@ -324,10 +314,6 @@ class XmlFileLoader extends FileLoader $tags = $this->getChildren($service, 'tag'); - if (!empty($defaults['tags'])) { - $tags = array_merge($tags, $defaults['tags']); - } - foreach ($tags as $tag) { $parameters = []; foreach ($tag->attributes as $name => $node) { @@ -349,16 +335,17 @@ class XmlFileLoader extends FileLoader $definition->addTag($tag->getAttribute('name'), $parameters); } + $definition->setTags(array_merge_recursive($definition->getTags(), $defaults->getTags())); + $bindings = $this->getArgumentsAsPhp($service, 'bind', $file); $bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING; foreach ($bindings as $argument => $value) { $bindings[$argument] = new BoundArgument($value, true, $bindingType, $file); } - if (isset($defaults['bind'])) { - // deep clone, to avoid multiple process of the same instance in the passes - $bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings); - } + // deep clone, to avoid multiple process of the same instance in the passes + $bindings = array_merge(unserialize(serialize($defaults->getBindings())), $bindings); + if ($bindings) { $definition->setBindings($bindings); } @@ -443,7 +430,7 @@ class XmlFileLoader extends FileLoader // resolve definitions uksort($definitions, 'strnatcmp'); foreach (array_reverse($definitions) as $id => list($domElement, $file)) { - if (null !== $definition = $this->parseDefinition($domElement, $file, [])) { + if (null !== $definition = $this->parseDefinition($domElement, $file, new Definition())) { $this->setDefinition($id, $definition); } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 67865a2f2a..75ed3ad62e 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -315,19 +315,20 @@ class YamlFileLoader extends FileLoader * * @throws InvalidArgumentException When tags are invalid */ - private function parseDefinition(string $id, $service, string $file, array $defaults) + private function parseDefinition(string $id, $service, string $file, array $defaults, bool $return = false) { if (preg_match('/^_[a-zA-Z0-9_]*$/', $id)) { throw new InvalidArgumentException(sprintf('Service names that start with an underscore are reserved. Rename the "%s" service or define it in XML instead.', $id)); } if (\is_string($service) && 0 === strpos($service, '@')) { - $this->container->setAlias($id, $alias = new Alias(substr($service, 1))); + $alias = new Alias(substr($service, 1)); + if (isset($defaults['public'])) { $alias->setPublic($defaults['public']); } - return; + return $return ? $alias : $this->container->setAlias($id, $alias); } if (\is_array($service) && $this->isUsingShortSyntax($service)) { @@ -342,10 +343,52 @@ class YamlFileLoader extends FileLoader throw new InvalidArgumentException(sprintf('A service definition must be an array or a string starting with "@" but "%s" found for service "%s" in "%s". Check your YAML syntax.', get_debug_type($service), $id, $file)); } + if (isset($service['stack'])) { + if (!\is_array($service['stack'])) { + throw new InvalidArgumentException(sprintf('A stack must be an array of definitions, "%s" given for service "%s" in "%s". Check your YAML syntax.', get_debug_type($service), $id, $file)); + } + + $stack = []; + + foreach ($service['stack'] as $k => $frame) { + if (\is_array($frame) && 1 === \count($frame) && !isset(self::$serviceKeywords[key($frame)])) { + $frame = [ + 'class' => key($frame), + 'arguments' => current($frame), + ]; + } + + if (\is_array($frame) && isset($frame['stack'])) { + throw new InvalidArgumentException(sprintf('Service stack "%s" cannot contain another stack in "%s".', $id, $file)); + } + + $definition = $this->parseDefinition($id.'" at index "'.$k, $frame, $file, $defaults, true); + + if ($definition instanceof Definition) { + $definition->setInstanceofConditionals($this->instanceof); + } + + $stack[$k] = $definition; + } + + if ($diff = array_diff(array_keys($service), ['stack', 'public', 'deprecated'])) { + throw new InvalidArgumentException(sprintf('Invalid attribute "%s"; supported ones are "public" and "deprecated" for service "%s" in "%s". Check your YAML syntax.', implode('", "', $diff), $id, $file)); + } + + $service = [ + 'parent' => '', + 'arguments' => $stack, + 'tags' => ['container.stack'], + 'public' => $service['public'] ?? null, + 'deprecated' => $service['deprecated'] ?? null, + ]; + } + $this->checkDefinition($id, $service, $file); if (isset($service['alias'])) { - $this->container->setAlias($id, $alias = new Alias($service['alias'])); + $alias = new Alias($service['alias']); + if (isset($service['public'])) { $alias->setPublic($service['public']); } elseif (isset($defaults['public'])) { @@ -372,7 +415,7 @@ class YamlFileLoader extends FileLoader } } - return; + return $return ? $alias : $this->container->setAlias($id, $alias); } if ($this->isLoadingInstanceof) { @@ -426,7 +469,7 @@ class YamlFileLoader extends FileLoader $definition->setAbstract($service['abstract']); } - if (\array_key_exists('deprecated', $service)) { + if (isset($service['deprecated'])) { $deprecation = \is_array($service['deprecated']) ? $service['deprecated'] : ['message' => $service['deprecated']]; if (!isset($deprecation['package'])) { @@ -601,6 +644,14 @@ class YamlFileLoader extends FileLoader throw new InvalidArgumentException(sprintf('A "resource" attribute must be set when the "namespace" attribute is set for service "%s" in "%s". Check your YAML syntax.', $id, $file)); } + if ($return) { + if (\array_key_exists('resource', $service)) { + throw new InvalidArgumentException(sprintf('Invalid "resource" attribute found for service "%s" in "%s". Check your YAML syntax.', $id, $file)); + } + + return $definition; + } + if (\array_key_exists('resource', $service)) { if (!\is_string($service['resource'])) { throw new InvalidArgumentException(sprintf('A "resource" attribute must be of type string for service "%s" in "%s". Check your YAML syntax.', $id, $file)); diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index 673cf9cbe0..55c26ffdea 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -57,6 +57,7 @@ + @@ -176,6 +177,15 @@ + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/stack.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/stack.php new file mode 100644 index 0000000000..8a4d7ca19a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/stack.php @@ -0,0 +1,50 @@ +services(); + + $services->stack('stack_a', [ + service('stdClass') + ->property('label', 'A') + ->property('inner', ref('.inner')), + service('stdClass') + ->property('label', 'B') + ->property('inner', ref('.inner')), + service('stdClass') + ->property('label', 'C'), + ])->public(); + + $services->stack('stack_abstract', [ + service('stdClass') + ->property('label', 'A') + ->property('inner', ref('.inner')), + service('stdClass') + ->property('label', 'B') + ->property('inner', ref('.inner')), + ]); + + $services->stack('stack_b', [ + ref('stack_abstract'), + service('stdClass') + ->property('label', 'C'), + ])->public(); + + $services->stack('stack_c', [ + service('stdClass') + ->property('label', 'Z') + ->property('inner', ref('.inner')), + ref('stack_a'), + ])->public(); + + $services->stack('stack_d', [ + service() + ->parent('stack_abstract') + ->property('label', 'Z'), + service('stdClass') + ->property('label', 'C'), + ])->public(); +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/stack.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/stack.xml new file mode 100644 index 0000000000..5fd0796494 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/stack.xml @@ -0,0 +1,53 @@ + + + + + + A + + + + B + + + + C + + + + + + A + + + + B + + + + + + + + C + + + + + + Z + + + + + + + + Z + + + C + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/stack.yaml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/stack.yaml new file mode 100644 index 0000000000..ba4906ceb1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/stack.yaml @@ -0,0 +1,67 @@ +services: + stack_short: + stack: + - stdClass: [1, 2] + + stack_a: + public: true + stack: + - class: stdClass + properties: + label: A + inner: '@.inner' + - class: stdClass + properties: + label: B + inner: '@.inner' + - class: stdClass + properties: + label: C + + stack_abstract: + stack: + - class: stdClass + abstract: true + properties: + label: A + inner: '@.inner' + - class: stdClass + properties: + label: B + inner: '@.inner' + + stack_b: + public: true + stack: + - alias: 'stack_abstract' + - class: stdClass + properties: + label: C + + stack_c: + public: true + stack: + - class: stdClass + properties: + label: Z + inner: '@.inner' + - '@stack_a' + + stack_d: + public: true + stack: + - parent: 'stack_abstract' + properties: + label: 'Z' + - class: stdClass + properties: + label: C + + stack_e: + public: true + stack: + - class: stdClass + properties: + label: Y + inner: '@.inner' + - '@stack_d' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index 67524ff0df..9b3b6b5ba4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -104,6 +104,38 @@ class PhpFileLoaderTest extends TestCase $container->compile(); } + public function testStack() + { + $container = new ContainerBuilder(); + + $loader = new PhpFileLoader($container, new FileLocator(realpath(__DIR__.'/../Fixtures').'/config')); + $loader->load('stack.php'); + + $container->compile(); + + $expected = (object) [ + 'label' => 'A', + 'inner' => (object) [ + 'label' => 'B', + 'inner' => (object) [ + 'label' => 'C', + ], + ], + ]; + $this->assertEquals($expected, $container->get('stack_a')); + $this->assertEquals($expected, $container->get('stack_b')); + + $expected = (object) [ + 'label' => 'Z', + 'inner' => $expected, + ]; + $this->assertEquals($expected, $container->get('stack_c')); + + $expected = $expected->inner; + $expected->label = 'Z'; + $this->assertEquals($expected, $container->get('stack_d')); + } + /** * @group legacy */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 58003b12b3..5f74afd245 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -1044,4 +1044,36 @@ class XmlFileLoaderTest extends TestCase $arguments = $container->getDefinition(FooWithAbstractArgument::class)->getArguments(); $this->assertInstanceOf(AbstractArgument::class, $arguments['$baz']); } + + public function testStack() + { + $container = new ContainerBuilder(); + + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('stack.xml'); + + $container->compile(); + + $expected = (object) [ + 'label' => 'A', + 'inner' => (object) [ + 'label' => 'B', + 'inner' => (object) [ + 'label' => 'C', + ], + ], + ]; + $this->assertEquals($expected, $container->get('stack_a')); + $this->assertEquals($expected, $container->get('stack_b')); + + $expected = (object) [ + 'label' => 'Z', + 'inner' => $expected, + ]; + $this->assertEquals($expected, $container->get('stack_c')); + + $expected = $expected->inner; + $expected->label = 'Z'; + $this->assertEquals($expected, $container->get('stack_d')); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 3d90fd4c8f..bd46044e4f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -962,4 +962,44 @@ class YamlFileLoaderTest extends TestCase $this->assertSame($expected, $container->getDefinition('foo')->getMethodCalls()); } + + public function testStack() + { + $container = new ContainerBuilder(); + + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('stack.yaml'); + + $this->assertSame([1, 2], $container->getDefinition('stack_short')->getArguments()[0]->getArguments()); + + $container->compile(); + + $expected = (object) [ + 'label' => 'A', + 'inner' => (object) [ + 'label' => 'B', + 'inner' => (object) [ + 'label' => 'C', + ], + ], + ]; + $this->assertEquals($expected, $container->get('stack_a')); + $this->assertEquals($expected, $container->get('stack_b')); + + $expected = (object) [ + 'label' => 'Z', + 'inner' => $expected, + ]; + $this->assertEquals($expected, $container->get('stack_c')); + + $expected = $expected->inner; + $expected->label = 'Z'; + $this->assertEquals($expected, $container->get('stack_d')); + + $expected = (object) [ + 'label' => 'Y', + 'inner' => $expected, + ]; + $this->assertEquals($expected, $container->get('stack_e')); + } }