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