Skip to content

Commit 049f15e

Browse files
kbondfabpot
authored andcommitted
[DependencyInjection] Allow service subscribers to return SubscribedService[]
1 parent 79ee32a commit 049f15e

File tree

10 files changed

+179
-46
lines changed

10 files changed

+179
-46
lines changed

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
2828
use Symfony\Component\DependencyInjection\Reference;
2929
use Symfony\Component\DependencyInjection\TypedReference;
30+
use Symfony\Contracts\Service\Attribute\SubscribedService;
3031

3132
/**
3233
* Inspects existing service definitions and wires the autowired ones using the type hints of their classes.
@@ -104,6 +105,19 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
104105
private function doProcessValue(mixed $value, bool $isRoot = false): mixed
105106
{
106107
if ($value instanceof TypedReference) {
108+
if ($attributes = $value->getAttributes()) {
109+
$attribute = array_pop($attributes);
110+
111+
if ($attributes) {
112+
throw new AutowiringFailedException($this->currentId, sprintf('Using multiple attributes with "%s" is not supported.', SubscribedService::class));
113+
}
114+
115+
if (!$attribute instanceof Target) {
116+
return $this->processAttribute($attribute, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $value->getInvalidBehavior());
117+
}
118+
119+
$value = new TypedReference($value->getType(), $value->getType(), $value->getInvalidBehavior(), $attribute->name);
120+
}
107121
if ($ref = $this->getAutowiredReference($value, true)) {
108122
return $ref;
109123
}
@@ -158,6 +172,29 @@ private function doProcessValue(mixed $value, bool $isRoot = false): mixed
158172
return $value;
159173
}
160174

175+
private function processAttribute(object $attribute, bool $isOptional = false): mixed
176+
{
177+
switch (true) {
178+
case $attribute instanceof Autowire:
179+
$value = $this->container->getParameterBag()->resolveValue($attribute->value);
180+
181+
return $value instanceof Reference && $isOptional ? new Reference($value, ContainerInterface::NULL_ON_INVALID_REFERENCE) : $value;
182+
183+
case $attribute instanceof TaggedIterator:
184+
return new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, false, $attribute->defaultPriorityMethod, (array) $attribute->exclude);
185+
186+
case $attribute instanceof TaggedLocator:
187+
return new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, true, $attribute->defaultPriorityMethod, (array) $attribute->exclude));
188+
189+
case $attribute instanceof MapDecorated:
190+
$definition = $this->container->getDefinition($this->currentId);
191+
192+
return new Reference($definition->innerServiceId ?? $this->currentId.'.inner', $definition->decorationOnInvalid ?? ContainerInterface::NULL_ON_INVALID_REFERENCE);
193+
}
194+
195+
throw new AutowiringFailedException($this->currentId, sprintf('"%s" is an unsupported attribute.', $attribute::class));
196+
}
197+
161198
private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array
162199
{
163200
$this->decoratedId = null;
@@ -249,34 +286,8 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
249286

250287
if ($checkAttributes) {
251288
foreach ($parameter->getAttributes() as $attribute) {
252-
if (TaggedIterator::class === $attribute->getName()) {
253-
$attribute = $attribute->newInstance();
254-
$arguments[$index] = new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, false, $attribute->defaultPriorityMethod, (array) $attribute->exclude);
255-
break;
256-
}
257-
258-
if (TaggedLocator::class === $attribute->getName()) {
259-
$attribute = $attribute->newInstance();
260-
$arguments[$index] = new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, true, $attribute->defaultPriorityMethod, (array) $attribute->exclude));
261-
break;
262-
}
263-
264-
if (Autowire::class === $attribute->getName()) {
265-
$value = $attribute->newInstance()->value;
266-
$value = $this->container->getParameterBag()->resolveValue($value);
267-
268-
if ($value instanceof Reference && $parameter->allowsNull()) {
269-
$value = new Reference($value, ContainerInterface::NULL_ON_INVALID_REFERENCE);
270-
}
271-
272-
$arguments[$index] = $value;
273-
274-
break;
275-
}
276-
277-
if (MapDecorated::class === $attribute->getName()) {
278-
$definition = $this->container->getDefinition($this->currentId);
279-
$arguments[$index] = new Reference($definition->innerServiceId ?? $this->currentId.'.inner', $definition->decorationOnInvalid ?? ContainerInterface::NULL_ON_INVALID_REFERENCE);
289+
if (\in_array($attribute->getName(), [TaggedIterator::class, TaggedLocator::class, Autowire::class, MapDecorated::class], true)) {
290+
$arguments[$index] = $this->processAttribute($attribute->newInstance(), $parameter->allowsNull());
280291

281292
break;
282293
}
@@ -315,7 +326,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
315326
}
316327

317328
$getValue = function () use ($type, $parameter, $class, $method) {
318-
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, Target::parseName($parameter)), true)) {
329+
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, Target::parseName($parameter)), false)) {
319330
$failureMessage = $this->createTypeNotFoundMessageCallback($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
320331

321332
if ($parameter->isDefaultValueAvailable()) {

src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\DependencyInjection\Reference;
2121
use Symfony\Component\DependencyInjection\TypedReference;
2222
use Symfony\Component\HttpFoundation\Session\SessionInterface;
23+
use Symfony\Contracts\Service\Attribute\SubscribedService;
2324
use Symfony\Contracts\Service\ServiceProviderInterface;
2425
use Symfony\Contracts\Service\ServiceSubscriberInterface;
2526

@@ -73,6 +74,14 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
7374
$subscriberMap = [];
7475

7576
foreach ($class::getSubscribedServices() as $key => $type) {
77+
$attributes = [];
78+
79+
if ($type instanceof SubscribedService) {
80+
$key = $type->key;
81+
$attributes = $type->attributes;
82+
$type = ($type->nullable ? '?' : '').($type->type ?? throw new InvalidArgumentException(sprintf('When "%s::getSubscribedServices()" returns "%s", a type must be set.', $class, SubscribedService::class)));
83+
}
84+
7685
if (!\is_string($type) || !preg_match('/(?(DEFINE)(?<cn>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))(?(DEFINE)(?<fqcn>(?&cn)(?:\\\\(?&cn))*+))^\??(?&fqcn)(?:(?:\|(?&fqcn))*+|(?:&(?&fqcn))*+)$/', $type)) {
7786
throw new InvalidArgumentException(sprintf('"%s::getSubscribedServices()" must return valid PHP types for service "%s" key "%s", "%s" returned.', $class, $this->currentId, $key, \is_string($type) ? $type : get_debug_type($type)));
7887
}
@@ -109,7 +118,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
109118
$name = $this->container->has($type.' $'.$camelCaseName) ? $camelCaseName : $name;
110119
}
111120

112-
$subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $name);
121+
$subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $name, $attributes);
113122
unset($serviceMap[$key]);
114123
}
115124

src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Psr\Container\ContainerInterface as PsrContainerInterface;
1616
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
17+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
18+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
19+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
20+
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
21+
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
22+
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
23+
use Symfony\Component\DependencyInjection\Attribute\Target;
1724
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
1825
use Symfony\Component\DependencyInjection\Compiler\RegisterServiceSubscribersPass;
1926
use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass;
@@ -402,6 +409,65 @@ public static function getSubscribedServices(): array
402409
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
403410
}
404411

412+
public function testSubscribedServiceWithAttributes()
413+
{
414+
if (!property_exists(SubscribedService::class, 'attributes')) {
415+
$this->markTestSkipped('Requires symfony/service-contracts 3.2+');
416+
}
417+
418+
$container = new ContainerBuilder();
419+
420+
$subscriber = new class() implements ServiceSubscriberInterface {
421+
public static function getSubscribedServices(): array
422+
{
423+
return [
424+
new SubscribedService('tagged.iterator', 'iterable', attributes: new TaggedIterator('tag')),
425+
new SubscribedService('tagged.locator', PsrContainerInterface::class, attributes: new TaggedLocator('tag')),
426+
new SubscribedService('autowired', 'stdClass', attributes: new Autowire(service: 'service.id')),
427+
new SubscribedService('autowired.nullable', 'stdClass', nullable: true, attributes: new Autowire(service: 'service.id')),
428+
new SubscribedService('autowired.parameter', 'string', attributes: new Autowire('%parameter.1%')),
429+
new SubscribedService('map.decorated', \stdClass::class, attributes: new MapDecorated()),
430+
new SubscribedService('target', \stdClass::class, attributes: new Target('someTarget')),
431+
];
432+
}
433+
};
434+
435+
$container->setParameter('parameter.1', 'foobar');
436+
$container->register('foo', \get_class($subscriber))
437+
->addMethodCall('setContainer', [new Reference(PsrContainerInterface::class)])
438+
->addTag('container.service_subscriber');
439+
440+
(new RegisterServiceSubscribersPass())->process($container);
441+
(new ResolveServiceSubscribersPass())->process($container);
442+
443+
$foo = $container->getDefinition('foo');
444+
$locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]);
445+
446+
$expected = [
447+
'tagged.iterator' => new ServiceClosureArgument(new TypedReference('iterable', 'iterable', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'tagged.iterator', [new TaggedIterator('tag')])),
448+
'tagged.locator' => new ServiceClosureArgument(new TypedReference(PsrContainerInterface::class, PsrContainerInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'tagged.locator', [new TaggedLocator('tag')])),
449+
'autowired' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'autowired', [new Autowire(service: 'service.id')])),
450+
'autowired.nullable' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'autowired.nullable', [new Autowire(service: 'service.id')])),
451+
'autowired.parameter' => new ServiceClosureArgument(new TypedReference('string', 'string', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'autowired.parameter', [new Autowire(service: '%parameter.1%')])),
452+
'map.decorated' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'map.decorated', [new MapDecorated()])),
453+
'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'target', [new Target('someTarget')])),
454+
];
455+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
456+
457+
(new AutowirePass())->process($container);
458+
459+
$expected = [
460+
'tagged.iterator' => new ServiceClosureArgument(new TaggedIteratorArgument('tag')),
461+
'tagged.locator' => new ServiceClosureArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('tag', 'tag', needsIndexes: true))),
462+
'autowired' => new ServiceClosureArgument(new Reference('service.id')),
463+
'autowired.nullable' => new ServiceClosureArgument(new Reference('service.id', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
464+
'autowired.parameter' => new ServiceClosureArgument('foobar'),
465+
'map.decorated' => new ServiceClosureArgument(new Reference('.service_locator.oZHAdom.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
466+
'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'someTarget')),
467+
];
468+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
469+
}
470+
405471
public function testBinding()
406472
{
407473
$container = new ContainerBuilder();

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ public function isCompiled(): bool
4444
public function getRemovedIds(): array
4545
{
4646
return [
47-
'.service_locator.JmEob1b' => true,
48-
'.service_locator.KIgkoLM' => true,
49-
'.service_locator.qUb.lJI' => true,
50-
'.service_locator.qUb.lJI.foo_service' => true,
47+
'.service_locator.0H1ht0q' => true,
48+
'.service_locator.0H1ht0q.foo_service' => true,
49+
'.service_locator.2hyyc9y' => true,
50+
'.service_locator.KGUGnmw' => true,
5151
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,
5252
];
5353
}

src/Symfony/Component/DependencyInjection/TypedReference.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,21 @@ class TypedReference extends Reference
2020
{
2121
private string $type;
2222
private ?string $name;
23+
private array $attributes;
2324

2425
/**
2526
* @param string $id The service identifier
2627
* @param string $type The PHP type of the identified service
2728
* @param int $invalidBehavior The behavior when the service does not exist
2829
* @param string|null $name The name of the argument targeting the service
30+
* @param array $attributes The attributes to be used
2931
*/
30-
public function __construct(string $id, string $type, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, string $name = null)
32+
public function __construct(string $id, string $type, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, string $name = null, array $attributes = [])
3133
{
3234
$this->name = $type === $id ? $name : null;
3335
parent::__construct($id, $invalidBehavior);
3436
$this->type = $type;
37+
$this->attributes = $attributes;
3538
}
3639

3740
public function getType()
@@ -43,4 +46,9 @@ public function getName(): ?string
4346
{
4447
return $this->name;
4548
}
49+
50+
public function getAttributes(): array
51+
{
52+
return $this->attributes;
53+
}
4654
}

src/Symfony/Contracts/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
3.2
5+
---
6+
7+
* Allow `ServiceSubscriberInterface::getSubscribedServices()` to return `SubscribedService[]`
8+
49
3.0
510
---
611

src/Symfony/Contracts/Service/Attribute/SubscribedService.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111

1212
namespace Symfony\Contracts\Service\Attribute;
1313

14+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
1415
use Symfony\Contracts\Service\ServiceSubscriberTrait;
1516

1617
/**
18+
* For use as the return value for {@see ServiceSubscriberInterface}.
19+
*
20+
* @example new SubscribedService('http_client', HttpClientInterface::class, false, new Target('githubApi'))
21+
*
1722
* Use with {@see ServiceSubscriberTrait} to mark a method's return type
1823
* as a subscribed service.
1924
*
@@ -22,12 +27,21 @@
2227
#[\Attribute(\Attribute::TARGET_METHOD)]
2328
final class SubscribedService
2429
{
30+
/** @var object[] */
31+
public array $attributes;
32+
2533
/**
26-
* @param string|null $key The key to use for the service
27-
* If null, use "ClassName::methodName"
34+
* @param string|null $key The key to use for the service
35+
* @param class-string|null $type The service class
36+
* @param bool $nullable Whether the service is optional
37+
* @param object|object[] $attributes One or more dependency injection attributes to use
2838
*/
2939
public function __construct(
30-
public ?string $key = null
40+
public ?string $key = null,
41+
public ?string $type = null,
42+
public bool $nullable = false,
43+
array|object $attributes = [],
3144
) {
45+
$this->attributes = \is_array($attributes) ? $attributes : [$attributes];
3246
}
3347
}

src/Symfony/Contracts/Service/ServiceSubscriberInterface.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Contracts\Service;
1313

14+
use Symfony\Contracts\Service\Attribute\SubscribedService;
15+
1416
/**
1517
* A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method.
1618
*
@@ -29,7 +31,8 @@
2931
interface ServiceSubscriberInterface
3032
{
3133
/**
32-
* Returns an array of service types required by such instances, optionally keyed by the service names used internally.
34+
* Returns an array of service types (or {@see SubscribedService} objects) required
35+
* by such instances, optionally keyed by the service names used internally.
3336
*
3437
* For mandatory dependencies:
3538
*
@@ -47,7 +50,13 @@ interface ServiceSubscriberInterface
4750
* * ['?Psr\Log\LoggerInterface'] is a shortcut for
4851
* * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface']
4952
*
50-
* @return string[] The required service types, optionally keyed by service names
53+
* additionally, an array of {@see SubscribedService}'s can be returned:
54+
*
55+
* * [new SubscribedService('logger', Psr\Log\LoggerInterface::class)]
56+
* * [new SubscribedService(type: Psr\Log\LoggerInterface::class, nullable: true)]
57+
* * [new SubscribedService('http_client', HttpClientInterface::class, attributes: new Target('githubApi'))]
58+
*
59+
* @return string[]|SubscribedService[] The required service types, optionally keyed by service names
5160
*/
5261
public static function getSubscribedServices(): array;
5362
}

0 commit comments

Comments
 (0)