diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
index 5d1e803ab392f..e4ef2b291ddcd 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 b6d475c770ff6..9a70003e442eb 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php
@@ -51,6 +51,7 @@ public function __construct()
$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 0000000000000..61202adf33fbe
--- /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 2257edaef6daf..68b3cb5e94689 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractServiceConfigurator.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractServiceConfigurator.php
@@ -81,6 +81,18 @@ final public function get(string $id): ServiceConfigurator
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 a5e0084226743..42efb181dce1c 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\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 @@ final public function get(string $id): ServiceConfigurator
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 18843bf979e08..cc2073b062675 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
@@ -112,12 +112,12 @@ private function parseImports(\DOMDocument $xml, string $file)
}
}
- 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 @@ private function parseDefinitions(\DOMDocument $xml, string $file, array $defaul
$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 @@ private function parseDefinitions(\DOMDocument $xml, string $file, array $defaul
}
}
- /**
- * 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 [];
- }
-
- $bindings = [];
- foreach ($this->getArgumentsAsPhp($defaultsNode, 'bind', $file) as $argument => $value) {
- $bindings[$argument] = new BoundArgument($value, true, BoundArgument::DEFAULTS_BINDING, $file);
+ return new Definition();
}
- $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));
- }
- }
+ $defaultsNode->setAttribute('id', '');
- 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 @@ private function parseDefinition(\DOMElement $service, string $file, array $defa
$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 @@ private function parseDefinition(\DOMElement $service, string $file, array $defa
$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 @@ private function parseDefinition(\DOMElement $service, string $file, array $defa
$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 @@ private function parseDefinition(\DOMElement $service, string $file, array $defa
$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 @@ private function processAnonymousServices(\DOMDocument $xml, string $file)
// 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 cc1ae1d7a2498..20c10aeee3dfb 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
@@ -315,19 +315,20 @@ private function isUsingShortSyntax(array $service): bool
*
* @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 @@ private function parseDefinition(string $id, $service, string $file, array $defa
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 @@ private function parseDefinition(string $id, $service, string $file, array $defa
}
}
- return;
+ return $return ? $alias : $this->container->setAlias($id, $alias);
}
if ($this->isLoadingInstanceof) {
@@ -426,7 +469,7 @@ private function parseDefinition(string $id, $service, string $file, array $defa
$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 @@ private function parseDefinition(string $id, $service, string $file, array $defa
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 673cf9cbe0e9e..55c26ffdea963 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 0000000000000..8a4d7ca19a1de
--- /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 0000000000000..5fd0796494100
--- /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 0000000000000..ba4906ceb1f7d
--- /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 aa73547df2878..1192994e448b7 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php
@@ -101,6 +101,38 @@ public function testFactoryShortNotationNotAllowed()
$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
* @expectedDeprecation Since symfony/dependency-injection 5.1: The signature of method "Symfony\Component\DependencyInjection\Loader\Configurator\Traits\DeprecateTrait::deprecate()" requires 3 arguments: "string $package, string $version, string $message", not defining them is deprecated.
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
index 9698313cf07c3..6c8d2ffdcfb51 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
@@ -1039,4 +1039,36 @@ public function testLoadServiceWithAbstractArgument()
$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 42578dce3b03b..38d4c683c2bf9 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
@@ -958,4 +958,44 @@ public function testAlternativeMethodCalls()
$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'));
+ }
}