Skip to content

Commit 123842a

Browse files
kbondnicolas-grekas
authored andcommitted
[DependencyInjection] Fix support for unions/intersections together with ServiceSubscriberInterface
1 parent 0abd898 commit 123842a

8 files changed

+232
-5
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ protected function processValue($value, bool $isRoot = false)
103103
private function doProcessValue($value, bool $isRoot = false)
104104
{
105105
if ($value instanceof TypedReference) {
106-
if ($ref = $this->getAutowiredReference($value)) {
106+
if ($ref = $this->getAutowiredReference($value, true)) {
107107
return $ref;
108108
}
109109
if (ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
@@ -294,7 +294,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
294294
}
295295

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

300300
if ($parameter->isDefaultValueAvailable()) {
@@ -349,7 +349,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
349349
/**
350350
* Returns a reference to the service matching the given type, if any.
351351
*/
352-
private function getAutowiredReference(TypedReference $reference): ?TypedReference
352+
private function getAutowiredReference(TypedReference $reference, bool $filterType): ?TypedReference
353353
{
354354
$this->lastFailure = null;
355355
$type = $reference->getType();
@@ -358,6 +358,14 @@ private function getAutowiredReference(TypedReference $reference): ?TypedReferen
358358
return $reference;
359359
}
360360

361+
if ($filterType && false !== $m = strpbrk($type, '&|')) {
362+
$types = array_diff(explode($m[0], $type), ['int', 'string', 'array', 'bool', 'float', 'iterable', 'object', 'callable', 'null']);
363+
364+
sort($types);
365+
366+
$type = implode($m[0], $types);
367+
}
368+
361369
if (null !== $name = $reference->getName()) {
362370
if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) {
363371
return new TypedReference($alias, $type, $reference->getInvalidBehavior());

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ protected function processValue($value, bool $isRoot = false)
7272
$subscriberMap = [];
7373

7474
foreach ($class::getSubscribedServices() as $key => $type) {
75-
if (!\is_string($type) || !preg_match('/^\??[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $type)) {
75+
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)) {
7676
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)));
7777
}
7878
if ($optionalBehavior = '?' === $type[0]) {

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1754,7 +1754,7 @@ private function dumpValue($value, bool $interpolate = true): string
17541754

17551755
$returnedType = '';
17561756
if ($value instanceof TypedReference) {
1757-
$returnedType = sprintf(': %s\%s', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $value->getInvalidBehavior() ? '' : '?', $value->getType());
1757+
$returnedType = sprintf(': %s\%s', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $value->getInvalidBehavior() ? '' : '?', str_replace(['|', '&'], ['|\\', '&\\'], $value->getType()));
17581758
}
17591759

17601760
$code = sprintf('return %s;', $code);

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

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition3;
3232
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber;
3333
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberChild;
34+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberIntersection;
35+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberIntersectionWithTrait;
3436
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberParent;
37+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberUnion;
38+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberUnionWithTrait;
3539
use Symfony\Component\DependencyInjection\TypedReference;
3640
use Symfony\Contracts\Service\Attribute\SubscribedService;
3741
use Symfony\Contracts\Service\ServiceSubscriberInterface;
@@ -129,6 +133,78 @@ public function testWithAttributes()
129133
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
130134
}
131135

136+
/**
137+
* @requires PHP 8
138+
*/
139+
public function testUnionServices()
140+
{
141+
$container = new ContainerBuilder();
142+
143+
$container->register('bar', \stdClass::class);
144+
$container->setAlias(TestDefinition1::class, 'bar');
145+
$container->setAlias(TestDefinition2::class, 'bar');
146+
$container->register('foo', TestServiceSubscriberUnion::class)
147+
->addArgument(new Reference(PsrContainerInterface::class))
148+
->addTag('container.service_subscriber')
149+
;
150+
151+
(new RegisterServiceSubscribersPass())->process($container);
152+
(new ResolveServiceSubscribersPass())->process($container);
153+
154+
$foo = $container->getDefinition('foo');
155+
$locator = $container->getDefinition((string) $foo->getArgument(0));
156+
157+
$this->assertFalse($locator->isPublic());
158+
$this->assertSame(ServiceLocator::class, $locator->getClass());
159+
160+
$expected = [
161+
'string|'.TestDefinition2::class.'|'.TestDefinition1::class => new ServiceClosureArgument(new TypedReference('string|'.TestDefinition2::class.'|'.TestDefinition1::class, 'string|'.TestDefinition2::class.'|'.TestDefinition1::class)),
162+
'bar' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class.'|'.TestDefinition2::class, TestDefinition1::class.'|'.TestDefinition2::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'bar')),
163+
'baz' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class.'|'.TestDefinition2::class, TestDefinition1::class.'|'.TestDefinition2::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'baz')),
164+
];
165+
166+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
167+
168+
(new AutowirePass())->process($container);
169+
170+
$expected = [
171+
'string|'.TestDefinition2::class.'|'.TestDefinition1::class => new ServiceClosureArgument(new TypedReference('bar', TestDefinition1::class.'|'.TestDefinition2::class)),
172+
'bar' => new ServiceClosureArgument(new TypedReference('bar', TestDefinition1::class.'|'.TestDefinition2::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'bar')),
173+
'baz' => new ServiceClosureArgument(new TypedReference('bar', TestDefinition1::class.'|'.TestDefinition2::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'baz')),
174+
];
175+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
176+
}
177+
178+
/**
179+
* @requires PHP 8.1
180+
*/
181+
public function testIntersectionServices()
182+
{
183+
$container = new ContainerBuilder();
184+
185+
$container->register('foo', TestServiceSubscriberIntersection::class)
186+
->addArgument(new Reference(PsrContainerInterface::class))
187+
->addTag('container.service_subscriber')
188+
;
189+
190+
(new RegisterServiceSubscribersPass())->process($container);
191+
(new ResolveServiceSubscribersPass())->process($container);
192+
193+
$foo = $container->getDefinition('foo');
194+
$locator = $container->getDefinition((string) $foo->getArgument(0));
195+
196+
$this->assertFalse($locator->isPublic());
197+
$this->assertSame(ServiceLocator::class, $locator->getClass());
198+
199+
$expected = [
200+
TestDefinition1::class.'&'.TestDefinition2::class => new ServiceClosureArgument(new TypedReference(TestDefinition1::class.'&'.TestDefinition2::class, TestDefinition1::class.'&'.TestDefinition2::class)),
201+
'bar' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class.'&'.TestDefinition2::class, TestDefinition1::class.'&'.TestDefinition2::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'bar')),
202+
'baz' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class.'&'.TestDefinition2::class, TestDefinition1::class.'&'.TestDefinition2::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'baz')),
203+
];
204+
205+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
206+
}
207+
132208
public function testExtraServiceSubscriber()
133209
{
134210
$this->expectException(InvalidArgumentException::class);
@@ -306,6 +382,65 @@ public function method()
306382
$subscriber::getSubscribedServices();
307383
}
308384

385+
/**
386+
* @requires PHP 8
387+
*/
388+
public function testServiceSubscriberTraitWithUnionReturnType()
389+
{
390+
if (!class_exists(SubscribedService::class)) {
391+
$this->markTestSkipped('SubscribedService attribute not available.');
392+
}
393+
394+
$container = new ContainerBuilder();
395+
396+
$container->register('foo', TestServiceSubscriberUnionWithTrait::class)
397+
->addMethodCall('setContainer', [new Reference(PsrContainerInterface::class)])
398+
->addTag('container.service_subscriber')
399+
;
400+
401+
(new RegisterServiceSubscribersPass())->process($container);
402+
(new ResolveServiceSubscribersPass())->process($container);
403+
404+
$foo = $container->getDefinition('foo');
405+
$locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]);
406+
407+
$expected = [
408+
TestServiceSubscriberUnionWithTrait::class.'::method1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class.'|'.TestDefinition2::class.'|null', TestDefinition1::class.'|'.TestDefinition2::class.'|null', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
409+
TestServiceSubscriberUnionWithTrait::class.'::method2' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class.'|'.TestDefinition2::class, TestDefinition1::class.'|'.TestDefinition2::class)),
410+
];
411+
412+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
413+
}
414+
415+
/**
416+
* @requires PHP 8.1
417+
*/
418+
public function testServiceSubscriberTraitWithIntersectionReturnType()
419+
{
420+
if (!class_exists(SubscribedService::class)) {
421+
$this->markTestSkipped('SubscribedService attribute not available.');
422+
}
423+
424+
$container = new ContainerBuilder();
425+
426+
$container->register('foo', TestServiceSubscriberIntersectionWithTrait::class)
427+
->addMethodCall('setContainer', [new Reference(PsrContainerInterface::class)])
428+
->addTag('container.service_subscriber')
429+
;
430+
431+
(new RegisterServiceSubscribersPass())->process($container);
432+
(new ResolveServiceSubscribersPass())->process($container);
433+
434+
$foo = $container->getDefinition('foo');
435+
$locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]);
436+
437+
$expected = [
438+
TestServiceSubscriberIntersectionWithTrait::class.'::method1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class.'&'.TestDefinition2::class, TestDefinition1::class.'&'.TestDefinition2::class)),
439+
];
440+
441+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
442+
}
443+
309444
public function testServiceSubscriberWithSemanticId()
310445
{
311446
$container = new ContainerBuilder();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
6+
7+
class TestServiceSubscriberIntersection implements ServiceSubscriberInterface
8+
{
9+
public function __construct($container)
10+
{
11+
}
12+
13+
public static function getSubscribedServices(): array
14+
{
15+
return [
16+
TestDefinition1::class.'&'.TestDefinition2::class,
17+
'bar' => TestDefinition1::class.'&'.TestDefinition2::class,
18+
'baz' => '?'.TestDefinition1::class.'&'.TestDefinition2::class,
19+
];
20+
}
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Contracts\Service\Attribute\SubscribedService;
6+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
7+
use Symfony\Contracts\Service\ServiceSubscriberTrait;
8+
9+
class TestServiceSubscriberIntersectionWithTrait implements ServiceSubscriberInterface
10+
{
11+
use ServiceSubscriberTrait;
12+
13+
#[SubscribedService]
14+
private function method1(): TestDefinition1&TestDefinition2
15+
{
16+
return $this->container->get(__METHOD__);
17+
}
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
6+
7+
class TestServiceSubscriberUnion implements ServiceSubscriberInterface
8+
{
9+
public function __construct($container)
10+
{
11+
}
12+
13+
public static function getSubscribedServices(): array
14+
{
15+
return [
16+
'string|'.TestDefinition2::class.'|'.TestDefinition1::class,
17+
'bar' => TestDefinition1::class.'|'.TestDefinition2::class,
18+
'baz' => '?'.TestDefinition1::class.'|'.TestDefinition2::class,
19+
];
20+
}
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Contracts\Service\Attribute\SubscribedService;
6+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
7+
use Symfony\Contracts\Service\ServiceSubscriberTrait;
8+
9+
class TestServiceSubscriberUnionWithTrait implements ServiceSubscriberInterface
10+
{
11+
use ServiceSubscriberTrait;
12+
13+
#[SubscribedService]
14+
private function method1(): TestDefinition1|TestDefinition2|null
15+
{
16+
return $this->container->get(__METHOD__);
17+
}
18+
19+
#[SubscribedService]
20+
private function method2(): TestDefinition1|TestDefinition2
21+
{
22+
return $this->container->get(__METHOD__);
23+
}
24+
}

0 commit comments

Comments
 (0)