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

Skip to content

Commit aa868d8

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

File tree

11 files changed

+171
-19
lines changed

11 files changed

+171
-19
lines changed

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Add `enum` env var processor
1010
* Add `shuffle` env var processor
1111
* Add `resolve-env` option to `debug:config` command to display actual values of environment variables in dumped configuration
12+
* Allow `ServiceSubscriberInterface::getSubscribedServices()` to return `SubscribedService[]`
1213

1314
6.1
1415
---

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
103103

104104
private function doProcessValue(mixed $value, bool $isRoot = false): mixed
105105
{
106+
if ($value instanceof TypedReference) {
107+
$value = $this->processAttributes($value);
108+
}
109+
106110
if ($value instanceof TypedReference) {
107111
if ($ref = $this->getAutowiredReference($value, true)) {
108112
return $ref;
@@ -158,6 +162,43 @@ private function doProcessValue(mixed $value, bool $isRoot = false): mixed
158162
return $value;
159163
}
160164

165+
private function processAttributes(TypedReference $reference): mixed
166+
{
167+
if (!$attributes = $reference->getAttributes()) {
168+
return $reference;
169+
}
170+
171+
foreach ($attributes as $attribute) {
172+
// todo, remove duplication
173+
switch (true) {
174+
case $attribute instanceof Autowire:
175+
$value = $this->container->getParameterBag()->resolveValue($attribute->value);
176+
177+
if ($value instanceof Reference) {
178+
$value = new Reference($value, $reference->getInvalidBehavior());
179+
}
180+
181+
return $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+
case $attribute instanceof Target:
195+
return new Reference($reference->getType().' $'.$attribute->name);
196+
}
197+
}
198+
199+
return $reference;
200+
}
201+
161202
private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array
162203
{
163204
$this->decoratedId = null;

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

src/Symfony/Contracts/Service/ServiceSubscriberTrait.php

Lines changed: 7 additions & 5 deletions
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)