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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy