diff --git a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php index 33168c007d477c4e9f225a2b960097884821248b..0c35209135bc4d0d110f9243c900c9356da8c046 100644 --- a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php +++ b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php @@ -31,6 +31,11 @@ */ class YamlFileLoader { + private const DEFAULTS_KEYWORDS = [ + 'public' => 'public', + 'tags' => 'tags', + 'autowire' => 'autowire', + ]; /** * @var \Drupal\Core\DependencyInjection\ContainerBuilder $container @@ -44,7 +49,6 @@ class YamlFileLoader */ protected $fileCache; - public function __construct(ContainerBuilder $container) { $this->container = $container; @@ -121,12 +125,71 @@ private function parseDefinitions($content, $file) $basename = basename($file); [$provider, ] = explode('.', $basename, 2); } + $defaults = $this->parseDefaults($content, $file); + $defaults['tags'][] = [ + 'name' => '_provider', + 'provider' => $provider + ]; foreach ($content['services'] as $id => $service) { - if (is_array($service)) { - $service['tags'][] = ['name' => '_provider', 'provider' => $provider]; + $this->parseDefinition($id, $service, $file, $defaults); + } + } + + /** + * @param array $content + * @param string $file + * + * @return array + * + * @throws InvalidArgumentException + */ + private function parseDefaults(array &$content, string $file): array + { + if (!\array_key_exists('_defaults', $content['services'])) { + return []; + } + $defaults = $content['services']['_defaults']; + unset($content['services']['_defaults']); + + if (!\is_array($defaults)) { + throw new InvalidArgumentException(sprintf('Service "_defaults" key must be an array, "%s" given in "%s".', \gettype($defaults), $file)); + } + + foreach ($defaults as $key => $default) { + if (!isset(self::DEFAULTS_KEYWORDS[$key])) { + throw new InvalidArgumentException(sprintf('The configuration key "%s" cannot be used to define a default value in "%s". Allowed keys are "%s".', $key, $file, implode('", "', self::DEFAULTS_KEYWORDS))); } - $this->parseDefinition($id, $service, $file); } + + if (isset($defaults['tags'])) { + if (!\is_array($tags = $defaults['tags'])) { + throw new InvalidArgumentException(sprintf('Parameter "tags" in "_defaults" must be an array in "%s". Check your YAML syntax.', $file)); + } + + foreach ($tags as $tag) { + if (!\is_array($tag)) { + $tag = ['name' => $tag]; + } + + if (!isset($tag['name'])) { + throw new InvalidArgumentException(sprintf('A "tags" entry in "_defaults" is missing a "name" key in "%s".', $file)); + } + $name = $tag['name']; + unset($tag['name']); + + if (!\is_string($name) || '' === $name) { + throw new InvalidArgumentException(sprintf('The tag name in "_defaults" must be a non-empty string in "%s".', $file)); + } + + foreach ($tag as $attribute => $value) { + if (!is_scalar($value) && null !== $value) { + throw new InvalidArgumentException(sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type in "%s". Check your YAML syntax.', $name, $attribute, $file)); + } + } + } + } + + return $defaults; } /** @@ -135,14 +198,18 @@ private function parseDefinitions($content, $file) * @param string $id * @param array $service * @param string $file + * @param array $defaults * * @throws InvalidArgumentException * When tags are invalid. */ - private function parseDefinition($id, $service, $file) + private function parseDefinition(string $id, $service, string $file, array $defaults) { - if (is_string($service) && 0 === strpos($service, '@')) { - $this->container->setAlias($id, substr($service, 1)); + if (\is_string($service) && 0 === strpos($service, '@')) { + $this->container->setAlias($id, $alias = new Alias(substr($service, 1))); + if (isset($defaults['public'])) { + $alias->setPublic($defaults['public']); + } return; } @@ -156,10 +223,11 @@ private function parseDefinition($id, $service, $file) } if (isset($service['alias'])) { - $alias = $this->container->setAlias($id, new Alias($service['alias'])); - - if (array_key_exists('public', $service)) { + $this->container->setAlias($id, $alias = new Alias($service['alias'])); + if (isset($service['public'])) { $alias->setPublic($service['public']); + } elseif (isset($defaults['public'])) { + $alias->setPublic($defaults['public']); } if (array_key_exists('deprecated', $service)) { @@ -176,6 +244,18 @@ private function parseDefinition($id, $service, $file) $definition = new Definition(); } + // Drupal services are public by default. + $definition->setPublic(true); + + if (isset($defaults['public'])) { + $definition->setPublic($defaults['public']); + } + if (isset($defaults['autowire'])) { + $definition->setAutowired($defaults['autowire']); + } + + $definition->setChanges([]); + if (isset($service['class'])) { $definition->setClass($service['class']); } @@ -195,9 +275,6 @@ private function parseDefinition($id, $service, $file) if (isset($service['public'])) { $definition->setPublic($service['public']); } - else { - $definition->setPublic(true); - } if (isset($service['abstract'])) { $definition->setAbstract($service['abstract']); @@ -271,31 +348,37 @@ private function parseDefinition($id, $service, $file) } } - if (isset($service['tags'])) { - if (!is_array($service['tags'])) { - throw new InvalidArgumentException(sprintf('Parameter "tags" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file)); - } + $tags = $service['tags'] ?? []; + if (!\is_array($tags)) { + throw new InvalidArgumentException(sprintf('Parameter "tags" must be an array for service "%s" in "%s". Check your YAML syntax.', $id, $file)); + } - foreach ($service['tags'] as $tag) { - if (!is_array($tag)) { - throw new InvalidArgumentException(sprintf('A "tags" entry must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file)); - } + if (isset($defaults['tags'])) { + $tags = array_merge($tags, $defaults['tags']); + } - if (!isset($tag['name'])) { - throw new InvalidArgumentException(sprintf('A "tags" entry is missing a "name" key for service "%s" in %s.', $id, $file)); - } + foreach ($tags as $tag) { + if (!\is_array($tag)) { + $tag = ['name' => $tag]; + } - $name = $tag['name']; - unset($tag['name']); + if (!isset($tag['name'])) { + throw new InvalidArgumentException(sprintf('A "tags" entry is missing a "name" key for service "%s" in "%s".', $id, $file)); + } + $name = $tag['name']; + unset($tag['name']); - foreach ($tag as $attribute => $value) { - if (!is_scalar($value) && null !== $value) { - throw new InvalidArgumentException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in %s. Check your YAML syntax.', $id, $name, $attribute, $file)); - } - } + if (!\is_string($name) || '' === $name) { + throw new InvalidArgumentException(sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', $id, $file)); + } - $definition->addTag($name, $tag); + foreach ($tag as $attribute => $value) { + if (!is_scalar($value) && null !== $value) { + throw new InvalidArgumentException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in "%s". Check your YAML syntax.', $id, $name, $attribute, $file)); + } } + + $definition->addTag($name, $tag); } if (isset($service['decorates'])) { diff --git a/core/modules/system/tests/modules/services_defaults_test/services_defaults_test.info.yml b/core/modules/system/tests/modules/services_defaults_test/services_defaults_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..2c81b06d451203b733341c62f9e7356eea3488a9 --- /dev/null +++ b/core/modules/system/tests/modules/services_defaults_test/services_defaults_test.info.yml @@ -0,0 +1,5 @@ +name: 'Services _defaults test' +type: module +description: 'Support module for services _defaults' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/services_defaults_test/services_defaults_test.services.yml b/core/modules/system/tests/modules/services_defaults_test/services_defaults_test.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..7206e711bb3c7f09552a3ffdcb3ba5197c09fb7f --- /dev/null +++ b/core/modules/system/tests/modules/services_defaults_test/services_defaults_test.services.yml @@ -0,0 +1,29 @@ +services: + _defaults: + autowire: true + public: false + tags: + - 'foo.tag1' + - { name: bar.tag2, test: 123 } + - { name: bar.tag3, value: null } + # Use an alias so the interface autowiring is tested. + Drupal\services_defaults_test\TestInjectionInterface: '@Drupal\services_defaults_test\TestInjection' + # A service that implements TestInjectionInterface. + Drupal\services_defaults_test\TestInjection: + public: true + Drupal\services_defaults_test\TestInjection2: + public: true + tags: + - 'zee.bang' + - { name: bar.tag2, test: 321 } + Drupal\services_defaults_test\TestService: + public: true + Drupal\services_defaults_test\TestPrivateService: ~ + 'services_default_test.no_autowire': + class: 'Drupal\services_defaults_test\TestService' + autowire: false + arguments: ['@services_default_test.no_autowire.arg', '@Drupal\services_defaults_test\TestInjection2'] + public: true + 'services_default_test.no_autowire.arg': + class: 'Drupal\services_defaults_test\TestInjection' + public: true diff --git a/core/modules/system/tests/modules/services_defaults_test/src/TestInjection.php b/core/modules/system/tests/modules/services_defaults_test/src/TestInjection.php new file mode 100644 index 0000000000000000000000000000000000000000..55ced1c887c19800ac1cf71120ea77a2d190e6a8 --- /dev/null +++ b/core/modules/system/tests/modules/services_defaults_test/src/TestInjection.php @@ -0,0 +1,9 @@ +<?php + +namespace Drupal\services_defaults_test; + +/** + * A service that is injected via default autowiring. + */ +class TestInjection implements TestInjectionInterface { +} diff --git a/core/modules/system/tests/modules/services_defaults_test/src/TestInjection2.php b/core/modules/system/tests/modules/services_defaults_test/src/TestInjection2.php new file mode 100644 index 0000000000000000000000000000000000000000..150f505dfd8e380af58072fef42b70a6f02a9061 --- /dev/null +++ b/core/modules/system/tests/modules/services_defaults_test/src/TestInjection2.php @@ -0,0 +1,9 @@ +<?php + +namespace Drupal\services_defaults_test; + +/** + * A service that is injected via default autowiring. + */ +class TestInjection2 { +} diff --git a/core/modules/system/tests/modules/services_defaults_test/src/TestInjectionInterface.php b/core/modules/system/tests/modules/services_defaults_test/src/TestInjectionInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..45a286c6137f7095d93c24b264543e58cdbb133c --- /dev/null +++ b/core/modules/system/tests/modules/services_defaults_test/src/TestInjectionInterface.php @@ -0,0 +1,9 @@ +<?php + +namespace Drupal\services_defaults_test; + +/** + * An interface for a service that is injected via default autowiring. + */ +interface TestInjectionInterface { +} diff --git a/core/modules/system/tests/modules/services_defaults_test/src/TestPrivateService.php b/core/modules/system/tests/modules/services_defaults_test/src/TestPrivateService.php new file mode 100644 index 0000000000000000000000000000000000000000..3b6e98a113256fa1d12106e89536d4ddefe126ff --- /dev/null +++ b/core/modules/system/tests/modules/services_defaults_test/src/TestPrivateService.php @@ -0,0 +1,9 @@ +<?php + +namespace Drupal\services_defaults_test; + +/** + * A service to test private flag. + */ +class TestPrivateService { +} diff --git a/core/modules/system/tests/modules/services_defaults_test/src/TestService.php b/core/modules/system/tests/modules/services_defaults_test/src/TestService.php new file mode 100644 index 0000000000000000000000000000000000000000..944a60bd5cfa83e2812ba6eb1e3f473194c7ae2f --- /dev/null +++ b/core/modules/system/tests/modules/services_defaults_test/src/TestService.php @@ -0,0 +1,33 @@ +<?php + +namespace Drupal\services_defaults_test; + +/** + * An autowired service to test _defaults. + */ +class TestService { + + /** + * @var \Drupal\services_defaults_test\TestInjectionInterface + */ + protected $testInjection; + + /** + * @var \Drupal\services_defaults_test\TestInjection2 + */ + protected $testInjection2; + + public function __construct(TestInjectionInterface $test_injection, TestInjection2 $test_injection2) { + $this->testInjection = $test_injection; + $this->testInjection2 = $test_injection2; + } + + public function getTestInjection() { + return $this->testInjection; + } + + public function getTestInjection2() { + return $this->testInjection2; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/DependencyInjection/ServicesDefaultsTest.php b/core/tests/Drupal/KernelTests/Core/DependencyInjection/ServicesDefaultsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8fdb9e2ffeb87e74383011cab96f664bdd81b3a0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/DependencyInjection/ServicesDefaultsTest.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\KernelTests\Core\DependencyInjection; + +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; + +/** + * Tests services _defaults definition. + * + * @group DependencyInjection + */ +class ServicesDefaultsTest extends KernelTestBase { + + protected static $modules = ['services_defaults_test']; + + /** + * Tests that 'services_defaults_test.service' has its dependencies injected. + */ + public function testAutowiring() { + // Ensure interface autowiring works. + $this->assertSame( + $this->container->get('Drupal\services_defaults_test\TestInjection'), + $this->container->get('Drupal\services_defaults_test\TestService')->getTestInjection() + ); + // Ensure defaults autowire works. + $this->assertSame( + $this->container->get('Drupal\services_defaults_test\TestInjection2'), + $this->container->get('Drupal\services_defaults_test\TestService')->getTestInjection2() + ); + + // Ensure that disabling autowiring works. + $this->assertNotSame( + $this->container->get('Drupal\services_defaults_test\TestInjection'), + $this->container->get('services_default_test.no_autowire')->getTestInjection() + ); + $this->assertSame( + $this->container->get('services_default_test.no_autowire.arg'), + $this->container->get('services_default_test.no_autowire')->getTestInjection() + ); + $this->assertSame( + $this->container->get('Drupal\services_defaults_test\TestInjection2'), + $this->container->get('services_default_test.no_autowire')->getTestInjection2() + ); + + } + + /** + * Tests that default tags for 'services_defaults_test.service' are applied. + */ + public function testDefaultTags() { + // Ensure default tags work. + $testServiceDefinition = $this->container->getDefinition('Drupal\services_defaults_test\TestService'); + $testInjection1Definition = $this->container->getDefinition('Drupal\services_defaults_test\TestInjection'); + $testInjection2Definition = $this->container->getDefinition('Drupal\services_defaults_test\TestInjection2'); + + $this->assertTrue($testServiceDefinition->hasTag('foo.tag1')); + $this->assertTrue($testServiceDefinition->hasTag('bar.tag2')); + $this->assertSame([['test' => 123]], $testServiceDefinition->getTag('bar.tag2')); + $this->assertTrue($testServiceDefinition->hasTag('bar.tag3')); + $this->assertSame([['value' => NULL]], $testServiceDefinition->getTag('bar.tag3')); + + $this->assertSame($testServiceDefinition->getTags(), $testInjection1Definition->getTags()); + + // Ensure overridden tag works. + $this->assertTrue($testInjection2Definition->hasTag('zee.bang')); + $this->assertTrue($testInjection2Definition->hasTag('foo.tag1')); + $this->assertTrue($testInjection2Definition->hasTag('bar.tag2')); + $this->assertSame([['test' => 321], ['test' => 123]], $testInjection2Definition->getTag('bar.tag2')); + $this->assertTrue($testInjection2Definition->hasTag('bar.tag3')); + $this->assertSame([['value' => NULL]], $testInjection2Definition->getTag('bar.tag3')); + } + + /** + * Tests that service from 'services_defaults_test.service' is private. + */ + public function testPrivateServices() { + // Ensure default and overridden public flag works. + $this->expectException(ServiceNotFoundException::class); + $this->container->getDefinition('Drupal\services_defaults_test\TestPrivateService'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php index 52728239ee84731ceb3c5f53a249e8e028803924..7354d304c3d959a3078c840ec0a2879ce983c8d7 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php @@ -58,4 +58,118 @@ class: \Drupal\Core\ExampleClass $this->assertSame('Drupal\Core\ExampleClass', $builder->getDefinition('Drupal\Core\ExampleClass')->getClass()); } + /** + * @dataProvider providerTestExceptions + */ + public function testExceptions($yml, $message) { + vfsStream::setup('drupal', NULL, [ + 'modules' => [ + 'example' => [ + 'example.yml' => $yml, + ], + ], + ]); + + $builder = new ContainerBuilder(); + $yaml_file_loader = new YamlFileLoader($builder); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($message); + $yaml_file_loader->load('vfs://drupal/modules/example/example.yml'); + } + + public function providerTestExceptions() { + return [ + '_defaults must be an array' => [<<<YAML +services: + _defaults: string +YAML, + 'Service "_defaults" key must be an array, "string" given in "vfs://drupal/modules/example/example.yml".', + ], + 'invalid _defaults key' => [<<<YAML +services: + _defaults: + invalid: string +YAML, + 'The configuration key "invalid" cannot be used to define a default value in "vfs://drupal/modules/example/example.yml". Allowed keys are "public", "tags", "autowire".', + ], + 'default tags must be an array' => [<<<YAML +services: + _defaults: + tags: string +YAML, + 'Parameter "tags" in "_defaults" must be an array in "vfs://drupal/modules/example/example.yml". Check your YAML syntax.', + ], + 'default tags must have a name' => [<<<YAML +services: + _defaults: + tags: + - {} +YAML, + 'A "tags" entry in "_defaults" is missing a "name" key in "vfs://drupal/modules/example/example.yml".', + ], + 'default tag name must not be empty' => [<<<YAML +services: + _defaults: + tags: + - '' +YAML, + 'The tag name in "_defaults" must be a non-empty string in "vfs://drupal/modules/example/example.yml".', + ], + 'default tag name must be a string' => [<<<YAML +services: + _defaults: + tags: + - 123 +YAML, + 'The tag name in "_defaults" must be a non-empty string in "vfs://drupal/modules/example/example.yml".', + ], + 'default tag attribute must be scalar' => [<<<YAML +services: + _defaults: + tags: + - { name: tag, value: [] } +YAML, + 'Tag "tag", attribute "value" in "_defaults" must be of a scalar-type in "vfs://drupal/modules/example/example.yml". Check your YAML syntax.', + ], + 'tags must be an array' => [<<<YAML +services: + service: + tags: string +YAML, + 'Parameter "tags" must be an array for service "service" in "vfs://drupal/modules/example/example.yml". Check your YAML syntax.', + ], + 'tags must have a name' => [<<<YAML +services: + service: + tags: + - {} +YAML, + 'A "tags" entry is missing a "name" key for service "service" in "vfs://drupal/modules/example/example.yml".', + ], + 'tag name must not be empty' => [<<<YAML +services: + service: + tags: + - '' +YAML, + 'The tag name for service "service" in "vfs://drupal/modules/example/example.yml" must be a non-empty string.', + ], + 'tag attribute must be scalar' => [<<<YAML +services: + service: + tags: + - { name: tag, value: [] } +YAML, + 'A "tags" attribute must be of a scalar-type for service "service", tag "tag", attribute "value" in "vfs://drupal/modules/example/example.yml". Check your YAML syntax.', + ], + 'service must be array or @service' => [<<<YAML +services: + service: string +YAML, + 'A service definition must be an array or a string starting with "@" but string found for service "service" in vfs://drupal/modules/example/example.yml. Check your YAML syntax.', + ], + ]; + } + }