8000 [DI] allow `ServiceSubscriberTrait` to autowire properties · symfony/symfony@187925e · GitHub
[go: up one dir, main page]

Skip to content

Commit 187925e

Browse files
committed
[DI] allow ServiceSubscriberTrait to autowire properties
1 parent 09a0edf commit 187925e

File tree

5 files changed

+86
-3
lines changed

5 files changed

+86
-3
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ public function testServiceSubscriberTraitWithSubscribedServiceAttribute()
239239
TestServiceSubscriberChild::class.'::testDefinition4' => new ServiceClosureArgument(new TypedReference(TestDefinition3::class, TestDefinition3::class)),
240240
TestServiceSubscriberParent::class.'::testDefinition1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class, TestDefinition1::class)),
241241
'custom_name' => new ServiceClosureArgument(new TypedReference(TestDefinition3::class, TestDefinition3::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'custom_name')),
242+
'testDefinition1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class, TestDefinition1::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'testDefinition1')),
243+
'testDefinition2' => new ServiceClosureArgument(new TypedReference(TestDefinition2::class, TestDefinition2::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'testDefinition2')),
242244
];
243245

244246
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ class TestServiceSubscriberChild extends TestServiceSubscriberParent
1010
use ServiceSubscriberTrait;
1111
use TestServiceSubscriberTrait;
1212

13+
#[SubscribedService]
14+
private TestDefinition1 $testDefinition1;
15+
16+
#[SubscribedService]
17+
private ?TestDefinition2 $testDefinition2;
18+
1319
#[SubscribedService]
1420
private function testDefinition2(): ?TestDefinition2
1521
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*
2020
* @author Kevin Bond <kevinbond@gmail.com>
2121
*/
22-
#[\Attribute(\Attribute::TARGET_METHOD)]
22+
#[\Attribute(\Attribute::TARGET_METHOD|\Attribute::TARGET_PROPERTY)]
2323
final class SubscribedService
2424
{
2525
/**

src/Symfony/Contracts/Service/ServiceSubscriberTrait.php

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
namespace Symfony\Contracts\Service;
1313

1414
use Psr\Container\ContainerInterface;
15+
use Psr\Container\NotFoundExceptionInterface;
1516
use Symfony\Contracts\Service\Attribute\Required;
1617
use Symfony\Contracts\Service\Attribute\SubscribedService;
1718

1819
/**
1920
* Implementation of ServiceSubscriberInterface that determines subscribed services from
20-
* method return types. Service ids are available as "ClassName::methodName".
21+
* method return types and property type-hints for methods/properties marked with the
22+
* "SubscribedService" attribute. Service ids are available as "ClassName::methodName"
23+
* for methods and "propertyName" for properties.
2124
*
2225
* @author Kevin Bond <kevinbond@gmail.com>
2326
*/
@@ -32,8 +35,37 @@ trait ServiceSubscriberTrait
3235
public static function getSubscribedServices(): array
3336
{
3437
$services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : [];
38+
$refClass = new \ReflectionClass(self::class);
3539

36-
foreach ((new \ReflectionClass(self::class))->getMethods() as $method) {
40+
foreach ($refClass->getProperties() as $property) {
41+
if (self::class !== $property->getDeclaringClass()->name) {
42+
continue;
43+
}
44+
45+
if (!$attribute = $property->getAttributes(SubscribedService::class)[0] ?? null) {
46+
continue;
47+
}
48+
49+
$type = $property->getType();
50+
51+
if ($property->isStatic() || !$type || $type->isBuiltin()) {
52+
throw new \LogicException(sprintf('Cannot use "%s" on property "%s::$%s" (can only be used on non-static properties with a non-native return type).', SubscribedService::class, self::class, $property->name));
53+
}
54+
55+
if ($attribute->newInstance()->key) {
56+
throw new \LogicException(sprintf('"%s" cannot set a custom name on property "%s::$%s".', SubscribedService::class, self::class, $property->name));
57+
}
58+
59+
$serviceId = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type;
60+
61+
if ($type->allowsNull()) {
62+
$serviceId = '?'.$serviceId;
63+
}
64+
65+
$services[$property->name] = $serviceId;
66+
}
67+
68+
foreach ($refClass->getMethods() as $method) {
3769
if (self::class !== $method->getDeclaringClass()->name) {
3870
continue;
3971
}
@@ -67,10 +99,28 @@ public function setContainer(ContainerInterface $container): ?ContainerInterface
6799
{
68100
$this->container = $container;
69101

102+
if ($container instanceof ServiceProviderInterface) {
103+
// TODO: what if this isn't an instance of ServiceProviderInterface?
104+
foreach (\array_keys($container->getProvidedServices()) as $key) {
105+
// change property from "uninitialized" to "unset"
106+
unset($this->$key);
107+
}
108+
}
109+
70110
if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) {
71111
return parent::setContainer($container);
72112
}
73113

74114
return null;
75115
}
116+
117+
public function __get(string $name): mixed
118+
{
119+
// TODO: ensure cannot be called from outside of the scope of the object?
120+
// TODO: what if class has a child/parent that allows this?
121+
// TODO: call parent::__get()?
122+
// TODO: how to handle null
123+
124+
return $this->$name = $this->container->get($name);
125+
}
76126
}

src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Component1\Dir2\Service2;
1818
use Symfony\Contracts\Service\Attribute\SubscribedService;
1919
use Symfony\Contracts\Service\ServiceLocatorTrait;
20+
use Symfony\Contracts\Service\ServiceProviderInterface;
2021
use Symfony\Contracts\Service\ServiceSubscriberInterface;
2122
use Symfony\Contracts\Service\ServiceSubscriberTrait;
2223

@@ -25,6 +26,8 @@ class ServiceSubscriberTraitTest extends TestCase
2526
public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices()
2627
{
2728
$expected = [
29+
'service1' => Service1::class,
30+
'service2' => '?'.Service2::class,
2831
TestService::class.'::aService' => Service2::class,
2932
TestService::class.'::nullableService' => '?'.Service2::class,
3033
];
@@ -66,6 +69,22 @@ public function testParentNotCalledIfNoParent()
6669
$this->assertNull($service->setContainer($container));
6770
$this->assertSame([], $service::getSubscribedServices());
6871
}
72+
73+
/**
74+
* @test
75+
*/
76+
public function can_get_subscribed_service_properties(): void
77+
{
78+
$factories = ['service1' => fn() => new Service1(), 'somethingElse' => fn() => new Service2()];
79+
$container = new class($factories) implements ServiceProviderInterface {
80+
use ServiceLocatorTrait;
81+
};
82+
$service = new TestService();
83+
$service->setContainer($container);
84+
85+
$this->assertInstanceOf(Service1::class, $service->service1);
86+
//$this->assertNull($service->service2); TODO: this doesn't work
87+
}
6988
}
7089

7190
class ParentTestService
@@ -84,6 +103,12 @@ class TestService extends ParentTestService implements ServiceSubscriberInterfac
84103
{
85104
use ServiceSubscriberTrait;
86105

106+
#[SubscribedService]
107+
public Service1 $service1;
108+
109+
#[SubscribedService]
110+
public ?Service2 $service2;
111+
87112
#[SubscribedService]
88113
public function aService(): Service2
89114
{

0 commit comments

Comments
 (0)
0