Skip to content

Commit 573b694

Browse files
committed
[DI] allow ServiceSubscriberTrait to autowire properties
1 parent ddaedd2 commit 573b694

File tree

5 files changed

+87
-3
lines changed

5 files changed

+87
-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
@@ -246,6 +246,8 @@ public function testServiceSubscriberTraitWithSubscribedServiceAttribute()
246246
TestServiceSubscriberChild::class.'::testDefinition4' => new ServiceClosureArgument(new TypedReference(TestDefinition3::class, TestDefinition3::class)),
247247
TestServiceSubscriberParent::class.'::testDefinition1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class, TestDefinition1::class)),
248248
'custom_name' => new ServiceClosureArgument(new TypedReference(TestDefinition3::class, TestDefinition3::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'custom_name')),
249+
'testDefinition1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class, TestDefinition1::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'testDefinition1')),
250+
'testDefinition2' => new ServiceClosureArgument(new TypedReference(TestDefinition2::class, TestDefinition2::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'testDefinition2')),
249251
];
250252

251253
$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
@@ -24,7 +24,7 @@
2424
*
2525
* @author Kevin Bond <kevinbond@gmail.com>
2626
*/
27-
#[\Attribute(\Attribute::TARGET_METHOD)]
27+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
2828
final class SubscribedService
2929
{
3030
/** @var object[] */

src/Symfony/Contracts/Service/ServiceSubscriberTrait.php

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
/**
1919
* Implementation of ServiceSubscriberInterface that determines subscribed services from
20-
* method return types. Service ids are available as "ClassName::methodName".
20+
* method return types and property type-hints for methods/properties marked with the
21+
* "SubscribedService" attribute. Service ids are available as "ClassName::methodName"
22+
* for methods and "propertyName" for properties.
2123
*
2224
* @author Kevin Bond <kevinbond@gmail.com>
2325
*/
@@ -29,8 +31,39 @@ trait ServiceSubscriberTrait
2931
public static function getSubscribedServices(): array
3032
{
3133
$services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : [];
34+
$refClass = new \ReflectionClass(self::class);
3235

33-
foreach ((new \ReflectionClass(self::class))->getMethods() as $method) {
36+
foreach ($refClass->getProperties() as $property) {
37+
if (self::class !== $property->getDeclaringClass()->name) {
38+
continue;
39+
}
40+
41+
if (!$attribute = $property->getAttributes(SubscribedService::class)[0] ?? null) {
42+
continue;
43+
}
44+
45+
if ($property->isStatic()) {
46+
throw new \LogicException(sprintf('Cannot use "%s" on property "%s::$%s" (can only be used on non-static properties with a type).', SubscribedService::class, self::class, $property->name));
47+
}
48+
49+
if (!$type = $property->getType()) {
50+
throw new \LogicException(sprintf('Cannot use "%s" on properties without a type in "%s::%s()".', SubscribedService::class, $property->name, self::class));
51+
}
52+
53+
/* @var SubscribedService $attribute */
54+
$attribute = $attribute->newInstance();
55+
$attribute->key ??= $property->name;
56+
$attribute->type ??= $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type;
57+
$attribute->nullable = $type->allowsNull();
58+
59+
if ($attribute->attributes) {
60+
$services[] = $attribute;
61+
} else {
62+
$services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type;
63+
}
64+
}
65+
66+
foreach ($refClass->getMethods() as $method) {
3467
if (self::class !== $method->getDeclaringClass()->name) {
3568
continue;
3669
}
@@ -68,10 +101,31 @@ public function setContainer(ContainerInterface $container): ?ContainerInterface
68101
{
69102
$this->container = $container;
70103

104+
foreach ((new \ReflectionClass(self::class))->getProperties() as $property) {
105+
if (self::class !== $property->getDeclaringClass()->name) {
106+
continue;
107+
}
108+
109+
if (!$property->getAttributes(SubscribedService::class)) {
110+
continue;
111+
}
112+
113+
unset($this->{$property->name});
114+
}
115+
71116
if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) {
72117
return parent::setContainer($container);
73118
}
74119

75120
return null;
76121
}
122+
123+
public function __get(string $name): mixed
124+
{
125+
// TODO: ensure cannot be called from outside of the scope of the object?
126+
// TODO: what if class has a child/parent that allows this?
127+
// TODO: call parent::__get()?
128+
129+
return $this->$name = $this->container->has($name) ? $this->container->get($name) : null;
130+
}
77131
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Contracts\Service\Attribute\Required;
1919
use Symfony\Contracts\Service\Attribute\SubscribedService;
2020
use Symfony\Contracts\Service\ServiceLocatorTrait;
21+
use Symfony\Contracts\Service\ServiceProviderInterface;
2122
use Symfony\Contracts\Service\ServiceSubscriberInterface;
2223
use Symfony\Contracts\Service\ServiceSubscriberTrait;
2324

@@ -26,6 +27,8 @@ class ServiceSubscriberTraitTest extends TestCase
2627
public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices()
2728
{
2829
$expected = [
30+
'service1' => Service1::class,
31+
'service2' => '?'.Service2::class,
2932
TestService::class.'::aService' => Service2::class,
3033
TestService::class.'::nullableService' => '?'.Service2::class,
3134
new SubscribedService(TestService::class.'::withAttribute', Service2::class, true, new Required()),
@@ -68,6 +71,19 @@ public function testParentNotCalledIfNoParent()
6871
$this->assertNull($service->setContainer($container));
6972
$this->assertSame([], $service::getSubscribedServices());
7073
}
74+
75+
public function testCanGetSubscribedServiceProperties(): void
76+
{
77+
$factories = ['service1' => fn () => new Service1(), 'somethingElse' => fn () => new Service2()];
78+
$container = new class($factories) implements ServiceProviderInterface {
79+
use ServiceLocatorTrait;
80+
};
81+
$service = new TestService();
82+
$service->setContainer($container);
83+
84+
$this->assertInstanceOf(Service1::class, $service->service1);
85+
$this->assertNull($service->service2);
86+
}
7187
}
7288

7389
class ParentTestService
@@ -86,6 +102,12 @@ class TestService extends ParentTestService implements ServiceSubscriberInterfac
86102
{
87103
use ServiceSubscriberTrait;
88104

105+
#[SubscribedService]
106+
public Service1 $service1;
107+
108+
#[SubscribedService]
109+
public ?Service2 $service2;
110+
89111
#[SubscribedService]
90112
public function aService(): Service2
91113
{

0 commit comments

Comments
 (0)