Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d921b76

Browse files
committed
[DI] allow service subscribers to return SubscribedService[]
1 parent 43c09ab commit d921b76

File tree

10 files changed

+187
-47
lines changed

10 files changed

+187
-47
lines changed

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

+53-28
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.
@@ -103,6 +104,10 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
103104

104105
private function doProcessValue(mixed $value, bool $isRoot = false): mixed
105106
{
107+
if ($value instanceof TypedReference) {
108+
$value = $this->processAttributes($value);
109+
}
110+
106111
if ($value instanceof TypedReference) {
107112
if ($ref = $this->getAutowiredReference($value, true)) {
108113
return $ref;
@@ -158,6 +163,52 @@ private function doProcessValue(mixed $value, bool $isRoot = false): mixed
158163
return $value;
159164
}
160165

166+
private function processAttribute(object $attribute, bool $isOptional = false): mixed
167+
{
168+
switch (true) {
169+
case $attribute instanceof Autowire:
170+
$value = $this->container->getParameterBag()->resolveValue($attribute->value);
171+
172+
if ($value instanceof Reference) {
173+
$value = new Reference($value, $isOptional ? ContainerInterface::NULL_ON_INVALID_REFERENCE : ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
174+
}
175+
176+
return $value;
177+
178+
case $attribute instanceof TaggedIterator:
179+
return new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, false, $attribute->defaultPriorityMethod, (array) $attribute->exclude);
180+
181+
case $attribute instanceof TaggedLocator:
182+
return new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, true, $attribute->defaultPriorityMethod, (array) $attribute->exclude));
183+
184+
case $attribute instanceof MapDecorated:
185+
$definition = $this->container->getDefinition($this->currentId);
186+
187+
return new Reference($definition->innerServiceId ?? $this->currentId.'.inner', $definition->decorationOnInvalid ?? ContainerInterface::NULL_ON_INVALID_REFERENCE);
188+
}
189+
190+
throw new AutowiringFailedException($this->currentId, sprintf('"%s" is an unsupported attribute.', $attribute::class));
191+
}
192+
193+
private function processAttributes(TypedReference $reference): mixed
194+
{
195+
if (!$attributes = $reference->getAttributes()) {
196+
return $reference;
197+
}
198+
199+
if (\count($attributes) > 1) {
200+
throw new AutowiringFailedException($this->currentId, sprintf('Using multiple attributes with "%s" is not supported.', SubscribedService::class));
201+
}
202+
203+
$attribute = array_values($attributes)[0];
204+
205+
if ($attribute instanceof Target) {
206+
return new Reference($reference->getType().' $'.$attribute->name, $reference->getInvalidBehavior());
207+
}
208+
209+
return $this->processAttribute($attribute, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior());
210+
}
211+
161212
private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array
162213
{
163214
$this->decoratedId = null;
@@ -249,34 +300,8 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
249300

250301
if ($checkAttributes) {
251302
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);
303+
if (\in_array($attribute->getName(), [TaggedIterator::class, TaggedLocator::class, Autowire::class, MapDecorated::class], true)) {
304+
$arguments[$index] = $this->processAttribute($attribute->newInstance(), $parameter->allowsNull());
280305

281306
break;
282307
}

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

+10-1
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"\'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

+67
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
1313

14+
use Composer\InstalledVersions;
1415
use PHPUnit\Framework\TestCase;
1516
use Psr\Container\ContainerInterface as PsrContainerInterface;
1617
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
18+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
19+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
20+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
21+
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
22+
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
23+
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
24+
use Symfony\Component\DependencyInjection\Attribute\Target;
1725
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
1826
use Symfony\Component\DependencyInjection\Compiler\RegisterServiceSubscribersPass;
1927
use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass;
@@ -402,6 +410,65 @@ public static function getSubscribedServices(): array
402410
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
403411
}
404412

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

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

+4-4
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

+9-1
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

+5
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

+17-3
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

+11-2
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
}

src/Symfony/Contracts/Service/ServiceSubscriberTrait.php

+7-5
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ public static function getSubscribedServices(): array
5050
throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class));
5151
}
5252

53-
$serviceId = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType;
53+
$attribute = $attribute->newInstance();
5454

55-
if ($returnType->allowsNull()) {
56-
$serviceId = '?'.$serviceId;
57-
}
55+
/* @var SubscribedService $attribute */
56+
57+
$attribute->key ??= self::class.'::'.$method->name;
58+
$attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType;
59+
$attribute->nullable = $returnType->allowsNull();
5860

59-
$services[$attribute->newInstance()->key ?? self::class.'::'.$method->name] = $serviceId;
61+
$services[] = $attribute;
6062
}
6163

6264
return $services;

0 commit comments

Comments
 (0)