From fc0aa24e89453e7656676e42975672299e79dc8e Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Mon, 1 Apr 2024 18:34:30 +0100 Subject: [PATCH] Issue #3414208 by kim.pepper, longwave, alexpott: Add support for tagged_iterator to YamlFileLoader --- .../DependencyInjection/Container.php | 11 +++++++ .../Dumper/OptimizedPhpArrayDumper.php | 22 +++++++++++-- .../Dumper/PhpArrayDumper.php | 4 --- .../DependencyInjection/PhpArrayContainer.php | 10 ++++++ .../Drupal/Component/Serialization/Yaml.php | 5 ++- .../DependencyInjection/YamlFileLoader.php | 29 +++++++++++++++- .../DependencyInjection/ContainerTest.php | 33 +++++++++++++++++++ .../Dumper/OptimizedPhpArrayDumperTest.php | 12 ++++++- .../YamlFileLoaderTest.php | 5 +++ 9 files changed, 121 insertions(+), 10 deletions(-) diff --git a/core/lib/Drupal/Component/DependencyInjection/Container.php b/core/lib/Drupal/Component/DependencyInjection/Container.php index ddc56b92d992..dc68c9c8a249 100644 --- a/core/lib/Drupal/Component/DependencyInjection/Container.php +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php @@ -2,6 +2,7 @@ namespace Drupal\Component\DependencyInjection; +use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -455,6 +456,16 @@ protected function resolveServicesAndParameters($arguments) { continue; } + elseif ($type == 'iterator') { + $services = $argument->value; + $arguments[$key] = new RewindableGenerator(function () use ($services) { + foreach ($services as $key => $service) { + yield $key => $this->resolveServicesAndParameters([$service])[0]; + } + }, count($services)); + + continue; + } // Check for collection. elseif ($type == 'collection') { $arguments[$key] = $this->resolveServicesAndParameters($argument->value); diff --git a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php index ec9f3f39e0ca..065e301e52fb 100644 --- a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php @@ -306,9 +306,6 @@ protected function dumpCollection($collection, &$resolve = FALSE) { $code = []; foreach ($collection as $key => $value) { - if ($value instanceof IteratorArgument) { - $value = $value->getValues(); - } if (is_array($value)) { $resolve_collection = FALSE; $code[$key] = $this->dumpCollection($value, $resolve_collection); @@ -438,6 +435,9 @@ protected function dumpValue($value) { return $this->getServiceClosureCall((string) $reference, $reference->getInvalidBehavior()); } + elseif ($value instanceof IteratorArgument) { + return $this->getIterator($value); + } elseif (is_object($value)) { throw new RuntimeException('Unable to dump a service container if a parameter is an object.'); } @@ -550,4 +550,20 @@ protected function getServiceClosureCall(string $id, int $invalid_behavior = Con ]; } + /** + * Gets a service iterator in a suitable PHP array format. + * + * @param \Symfony\Component\DependencyInjection\Argument\IteratorArgument $iterator + * The iterator. + * + * @return object + * The PHP array representation of the iterator. + */ + protected function getIterator(IteratorArgument $iterator) { + return (object) [ + 'type' => 'iterator', + 'value' => array_map($this->dumpValue(...), $iterator->getValues()), + ]; + } + } diff --git a/core/lib/Drupal/Component/DependencyInjection/Dumper/PhpArrayDumper.php b/core/lib/Drupal/Component/DependencyInjection/Dumper/PhpArrayDumper.php index 08451b11394e..a6fa2b9b09e1 100644 --- a/core/lib/Drupal/Component/DependencyInjection/Dumper/PhpArrayDumper.php +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/PhpArrayDumper.php @@ -2,7 +2,6 @@ namespace Drupal\Component\DependencyInjection\Dumper; -use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -34,9 +33,6 @@ protected function dumpCollection($collection, &$resolve = FALSE) { $code = []; foreach ($collection as $key => $value) { - if ($value instanceof IteratorArgument) { - $value = $value->getValues(); - } if (is_array($value)) { $code[$key] = $this->dumpCollection($value); } diff --git a/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php index fdb74e33c351..5241a9e4323f 100644 --- a/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php +++ b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php @@ -2,6 +2,7 @@ namespace Drupal\Component\DependencyInjection; +use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -174,6 +175,15 @@ protected function resolveServicesAndParameters($arguments) { continue; } + elseif ($type == 'iterator') { + $services = $argument->value; + $arguments[$key] = new RewindableGenerator(function () use ($services) { + foreach ($services as $key => $service) { + yield $key => $this->resolveServicesAndParameters([$service])[0]; + } + }, count($services)); + continue; + } if ($type !== NULL) { throw new InvalidArgumentException("Undefined type '$type' while resolving parameters and services."); diff --git a/core/lib/Drupal/Component/Serialization/Yaml.php b/core/lib/Drupal/Component/Serialization/Yaml.php index cb0835eed9b3..849512279b0e 100644 --- a/core/lib/Drupal/Component/Serialization/Yaml.php +++ b/core/lib/Drupal/Component/Serialization/Yaml.php @@ -34,7 +34,10 @@ public static function decode($raw) { $yaml = new Parser(); // Make sure we have a single trailing newline. A very simple config like // 'foo: bar' with no newline will fail to parse otherwise. - return $yaml->parse($raw, SymfonyYaml::PARSE_EXCEPTION_ON_INVALID_TYPE); + return $yaml->parse( + $raw, + SymfonyYaml::PARSE_EXCEPTION_ON_INVALID_TYPE | SymfonyYaml::PARSE_CUSTOM_TAGS + ); } catch (\Exception $e) { throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e); diff --git a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php index d165ace48a80..59e761246bb6 100644 --- a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php +++ b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php @@ -9,11 +9,14 @@ use Drupal\Component\Serialization\Exception\InvalidDataTypeException; use Drupal\Core\Serialization\Yaml; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\Yaml\Tag\TaggedValue; /** * YamlFileLoader loads YAML files service definitions. @@ -464,8 +467,32 @@ private function validate($content, $file) * * @return array|string|Reference */ - private function resolveServices($value) + private function resolveServices(mixed $value): mixed { + if ($value instanceof TaggedValue) { + $argument = $value->getValue(); + if (\in_array($value->getTag(), ['tagged', 'tagged_iterator', 'tagged_locator'], true)) { + $forLocator = 'tagged_locator' === $value->getTag(); + + if (\is_array($argument) && isset($argument['tag']) && $argument['tag']) { + if ($diff = array_diff(array_keys($argument), $supportedKeys = ['tag', 'index_by', 'default_index_method', 'default_priority_method', 'exclude', 'exclude_self'])) { + throw new InvalidArgumentException(sprintf('"!%s" tag contains unsupported key "%s"; supported ones are "%s".', $value->getTag(), implode('", "', $diff), implode('", "', $supportedKeys))); + } + + $argument = new TaggedIteratorArgument($argument['tag'], $argument['index_by'] ?? null, $argument['default_index_method'] ?? null, $forLocator, $argument['default_priority_method'] ?? null, (array) ($argument['exclude'] ?? null), $argument['exclude_self'] ?? true); + } elseif (\is_string($argument) && $argument) { + $argument = new TaggedIteratorArgument($argument, null, null, $forLocator); + } else { + throw new InvalidArgumentException(sprintf('"!%s" tags only accept a non empty string or an array with a key "tag"".', $value->getTag())); + } + + if ($forLocator) { + $argument = new ServiceLocatorArgument($argument); + } + + return $argument; + } + } if (is_array($value)) { $value = array_map(array($this, 'resolveServices'), $value); } elseif (is_string($value) && str_starts_with($value, '@=')) { diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php index 576e28854848..7f1be690a8ed 100644 --- a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php @@ -697,6 +697,19 @@ public function testResolveServicesAndParametersForRawArgument() { $this->assertEquals(['ccc'], $this->container->get('service_with_raw_argument')->getArguments()); } + /** + * Tests that service iterators are lazily instantiated. + */ + public function testIterator() { + $iterator = $this->container->get('service_iterator')->getArguments()[0]; + $this->assertIsIterable($iterator); + $this->assertFalse($this->container->initialized('other.service')); + foreach ($iterator as $service) { + $this->assertIsObject($service); + } + $this->assertTrue($this->container->initialized('other.service')); + } + /** * Tests Container::reset(). * @@ -975,6 +988,16 @@ protected function getMockContainerDefinition() { 'arguments' => $this->getCollection([$this->getRaw('ccc')]), ]; + // Iterator argument. + $services['service_iterator'] = [ + 'class' => '\Drupal\Tests\Component\DependencyInjection\MockInstantiationService', + 'arguments' => $this->getCollection([ + $this->getIterator([ + $this->getServiceCall('other.service'), + ]), + ]), + ]; + $aliases = []; $aliases['service.provider_alias'] = 'service.provider'; $aliases['late.service_alias'] = 'late.service'; @@ -1010,6 +1033,16 @@ protected function getServiceClosureCall($id, $invalid_behavior = ContainerInter ]; } + /** + * Helper function to return a service iterator. + */ + protected function getIterator($iterator) { + return (object) [ + 'type' => 'iterator', + 'value' => $iterator, + ]; + } + /** * Helper function to return a parameter definition. */ diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php index 1d80b0c885cb..df31c85d8a63 100644 --- a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php @@ -354,7 +354,7 @@ public static function getDefinitionsDataProvider() { $service_definitions[] = [ 'arguments' => [new IteratorArgument([new Reference('bar')])], 'arguments_count' => 1, - 'arguments_expected' => static::getCollection([static::getCollection([static::getServiceCall('bar')])]), + 'arguments_expected' => static::getCollection([static::getIterator([static::getServiceCall('bar')])]), ] + $base_service_definition; // Test a collection with a variable to resolve. @@ -695,6 +695,16 @@ protected static function getCollection($collection) { ]; } + /** + * Helper function to return a machine-optimized iterator. + */ + protected static function getIterator($collection) { + return (object) [ + 'type' => 'iterator', + 'value' => $collection, + ]; + } + /** * Helper function to return a parameter definition. */ diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php index dcf9c974afaa..56ed3e8894ed 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php @@ -9,6 +9,7 @@ use Drupal\Core\DependencyInjection\YamlFileLoader; use Drupal\Tests\UnitTestCase; use org\bovigo\vfs\vfsStream; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; /** * @coversDefaultClass \Drupal\Core\DependencyInjection\YamlFileLoader @@ -35,6 +36,9 @@ class: \Drupal\Core\ExampleClass class: \Drupal\Core\ExampleClass public: false Drupal\Core\ExampleClass: ~ + example_tagged_iterator: + class: \Drupal\Core\ExampleClass + arguments: [!tagged_iterator foo.bar]" YAML; vfsStream::setup('drupal', NULL, [ @@ -58,6 +62,7 @@ class: \Drupal\Core\ExampleClass $this->assertFalse($builder->has('example_private_service')); $this->assertTrue($builder->has('Drupal\Core\ExampleClass')); $this->assertSame('Drupal\Core\ExampleClass', $builder->getDefinition('Drupal\Core\ExampleClass')->getClass()); + $this->assertInstanceOf(TaggedIteratorArgument::class, $builder->getDefinition('example_tagged_iterator')->getArgument(0)); } /** -- GitLab