Skip to content
Snippets Groups Projects
Commit fc0aa24e authored by catch's avatar catch
Browse files

Issue #3414208 by kim.pepper, longwave, alexpott: Add support for tagged_iterator to YamlFileLoader

parent a65bbd5d
No related branches found
No related tags found
No related merge requests found
Showing with 121 additions and 10 deletions
......@@ -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);
......
......@@ -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()),
];
}
}
......@@ -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);
}
......
......@@ -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.");
......
......@@ -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);
......
......@@ -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, '@=')) {
......
......@@ -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.
*/
......
......@@ -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.
*/
......
......@@ -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));
}
/**
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment