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)
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