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

Issue #3021898 by longwave, alexpott, AaronBauman, daffie, opdavies: Support...

Issue #3021898 by longwave, alexpott, AaronBauman, daffie, opdavies: Support _defaults key in service.yml files for public, tags and autowire settings
parent 46e74432
No related branches found
No related tags found
No related merge requests found
Showing
with 415 additions and 32 deletions
......@@ -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'])) {
......
name: 'Services _defaults test'
type: module
description: 'Support module for services _defaults'
package: Testing
version: VERSION
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
<?php
namespace Drupal\services_defaults_test;
/**
* A service that is injected via default autowiring.
*/
class TestInjection implements TestInjectionInterface {
}
<?php
namespace Drupal\services_defaults_test;
/**
* A service that is injected via default autowiring.
*/
class TestInjection2 {
}
<?php
namespace Drupal\services_defaults_test;
/**
* An interface for a service that is injected via default autowiring.
*/
interface TestInjectionInterface {
}
<?php
namespace Drupal\services_defaults_test;
/**
* A service to test private flag.
*/
class TestPrivateService {
}
<?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;
}
}
<?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');
}
}
......@@ -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.',
],
];
}
}
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