From e2fb640892e30d855d90d7dfdc281fa5e5e0abaf Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 7 Jun 2022 17:48:08 -0400 Subject: [PATCH] [DI] allow `ServiceSubscriberTrait` to autowire properties --- .../RegisterServiceSubscribersPassTest.php | 2 + .../Fixtures/TestServiceSubscriberChild.php | 6 ++ .../Service/Attribute/SubscribedService.php | 2 +- .../Service/ServiceSubscriberTrait.php | 58 ++++++++++++++++++- .../Service/ServiceSubscriberTraitTest.php | 22 +++++++ 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index c2ecdf2f9f5ec..39d24fb657060 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -246,6 +246,8 @@ public function testServiceSubscriberTraitWithSubscribedServiceAttribute() TestServiceSubscriberChild::class.'::testDefinition4' => new ServiceClosureArgument(new TypedReference(TestDefinition3::class, TestDefinition3::class)), TestServiceSubscriberParent::class.'::testDefinition1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class, TestDefinition1::class)), 'custom_name' => new ServiceClosureArgument(new TypedReference(TestDefinition3::class, TestDefinition3::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'custom_name')), + 'testDefinition1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class, TestDefinition1::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'testDefinition1')), + 'testDefinition2' => new ServiceClosureArgument(new TypedReference(TestDefinition2::class, TestDefinition2::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'testDefinition2')), ]; $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberChild.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberChild.php index ee2df273996b6..ff7cf8fe1c68b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberChild.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberChild.php @@ -10,6 +10,12 @@ class TestServiceSubscriberChild extends TestServiceSubscriberParent use ServiceSubscriberTrait; use TestServiceSubscriberTrait; + #[SubscribedService] + private TestDefinition1 $testDefinition1; + + #[SubscribedService] + private ?TestDefinition2 $testDefinition2; + #[SubscribedService] private function testDefinition2(): ?TestDefinition2 { diff --git a/src/Symfony/Contracts/Service/Attribute/SubscribedService.php b/src/Symfony/Contracts/Service/Attribute/SubscribedService.php index d98e1dfdbbeb1..26775abbe3958 100644 --- a/src/Symfony/Contracts/Service/Attribute/SubscribedService.php +++ b/src/Symfony/Contracts/Service/Attribute/SubscribedService.php @@ -24,7 +24,7 @@ * * @author Kevin Bond */ -#[\Attribute(\Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] final class SubscribedService { /** @var object[] */ diff --git a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php index f7cd3a94158c1..b6629cd5a824a 100644 --- a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php +++ b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php @@ -17,7 +17,9 @@ /** * Implementation of ServiceSubscriberInterface that determines subscribed services from - * method return types. Service ids are available as "ClassName::methodName". + * method return types and property type-hints for methods/properties marked with the + * "SubscribedService" attribute. Service ids are available as "ClassName::methodName" + * for methods and "propertyName" for properties. * * @author Kevin Bond */ @@ -29,8 +31,39 @@ trait ServiceSubscriberTrait public static function getSubscribedServices(): array { $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; + $refClass = new \ReflectionClass(self::class); - foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + foreach ($refClass->getProperties() as $property) { + if (self::class !== $property->getDeclaringClass()->name) { + continue; + } + + if (!$attribute = $property->getAttributes(SubscribedService::class)[0] ?? null) { + continue; + } + + if ($property->isStatic()) { + 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)); + } + + if (!$type = $property->getType()) { + throw new \LogicException(sprintf('Cannot use "%s" on properties without a type in "%s::%s()".', SubscribedService::class, $property->name, self::class)); + } + + /* @var SubscribedService $attribute */ + $attribute = $attribute->newInstance(); + $attribute->key ??= $property->name; + $attribute->type ??= $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; + $attribute->nullable = $type->allowsNull(); + + if ($attribute->attributes) { + $services[] = $attribute; + } else { + $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; + } + } + + foreach ($refClass->getMethods() as $method) { if (self::class !== $method->getDeclaringClass()->name) { continue; } @@ -68,10 +101,31 @@ public function setContainer(ContainerInterface $container): ?ContainerInterface { $this->container = $container; + foreach ((new \ReflectionClass(self::class))->getProperties() as $property) { + if (self::class !== $property->getDeclaringClass()->name) { + continue; + } + + if (!$property->getAttributes(SubscribedService::class)) { + continue; + } + + unset($this->{$property->name}); + } + if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { return parent::setContainer($container); } return null; } + + public function __get(string $name): mixed + { + // TODO: ensure cannot be called from outside of the scope of the object? + // TODO: what if class has a child/parent that allows this? + // TODO: call parent::__get()? + + return $this->$name = $this->container->has($name) ? $this->container->get($name) : null; + } } diff --git a/src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php b/src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php index a4b2eccd899e9..76f3e9b8d441f 100644 --- a/src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php +++ b/src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php @@ -18,6 +18,7 @@ use Symfony\Contracts\Service\Attribute\Required; use Symfony\Contracts\Service\Attribute\SubscribedService; use Symfony\Contracts\Service\ServiceLocatorTrait; +use Symfony\Contracts\Service\ServiceProviderInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; use Symfony\Contracts\Service\ServiceSubscriberTrait; @@ -26,6 +27,8 @@ class ServiceSubscriberTraitTest extends TestCase public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices() { $expected = [ + 'service1' => Service1::class, + 'service2' => '?'.Service2::class, TestService::class.'::aService' => Service2::class, TestService::class.'::nullableService' => '?'.Service2::class, new SubscribedService(TestService::class.'::withAttribute', Service2::class, true, new Required()), @@ -68,6 +71,19 @@ public function testParentNotCalledIfNoParent() $this->assertNull($service->setContainer($container)); $this->assertSame([], $service::getSubscribedServices()); } + + public function testCanGetSubscribedServiceProperties() + { + $factories = ['service1' => fn () => new Service1(), 'somethingElse' => fn () => new Service2()]; + $container = new class($factories) implements ServiceProviderInterface { + use ServiceLocatorTrait; + }; + $service = new TestService(); + $service->setContainer($container); + + $this->assertInstanceOf(Service1::class, $service->service1); + $this->assertNull($service->service2); + } } class ParentTestService @@ -86,6 +102,12 @@ class TestService extends ParentTestService implements ServiceSubscriberInterfac { use ServiceSubscriberTrait; + #[SubscribedService] + public Service1 $service1; + + #[SubscribedService] + public ?Service2 $service2; + #[SubscribedService] public function aService(): Service2 { 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