From fe07cbc8d837f60caf7018068e350cc5163681a0 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 1 Nov 2023 09:14:07 +0100 Subject: [PATCH 1/9] [Tests] Streamline --- Test/ServiceLocatorTestCase.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Test/ServiceLocatorTestCase.php b/Test/ServiceLocatorTestCase.php index 583f72a78..65a3fe337 100644 --- a/Test/ServiceLocatorTestCase.php +++ b/Test/ServiceLocatorTestCase.php @@ -12,7 +12,9 @@ namespace Symfony\Contracts\Service\Test; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use Symfony\Contracts\Service\ServiceLocatorTrait; abstract class ServiceLocatorTestCase extends TestCase @@ -66,27 +68,29 @@ public function testGetDoesNotMemoize() public function testThrowsOnUndefinedInternalService() { - if (!$this->getExpectedException()) { - $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); - $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); - } $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, ]); + if (!$this->getExpectedException()) { + $this->expectException(NotFoundExceptionInterface::class); + $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); + } + $locator->get('foo'); } public function testThrowsOnCircularReference() { - $this->expectException(\Psr\Container\ContainerExceptionInterface::class); - $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, 'bar' => function () use (&$locator) { return $locator->get('baz'); }, 'baz' => function () use (&$locator) { return $locator->get('bar'); }, ]); + $this->expectException(ContainerExceptionInterface::class); + $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); + $locator->get('foo'); } } From cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 20 Dec 2023 13:09:40 -0500 Subject: [PATCH 2/9] [DependencyInjection] Add `ServiceCollectionInterface` --- ServiceCollectionInterface.php | 26 ++++++++++++++++++++++++++ composer.json | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 ServiceCollectionInterface.php diff --git a/ServiceCollectionInterface.php b/ServiceCollectionInterface.php new file mode 100644 index 000000000..2333139ce --- /dev/null +++ b/ServiceCollectionInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * A ServiceProviderInterface that is also countable and iterable. + * + * @author Kevin Bond + * + * @template-covariant T of mixed + * + * @extends ServiceProviderInterface + * @extends \IteratorAggregate + */ +interface ServiceCollectionInterface extends ServiceProviderInterface, \Countable, \IteratorAggregate +{ +} diff --git a/composer.json b/composer.json index 32bb8a316..061561c91 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", From a17e69656265139c2ab4a62529f495a2a41fd6ce Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 5 Apr 2024 11:11:55 +0200 Subject: [PATCH 3/9] [Contracts] Rename ServiceSubscriberTrait to ServiceMethodsSubscriberTrait --- Attribute/SubscribedService.php | 4 +- ServiceMethodsSubscriberTrait.php | 80 +++++++++++++++++++++++++++++++ ServiceSubscriberTrait.php | 16 +++++-- composer.json | 3 +- 4 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 ServiceMethodsSubscriberTrait.php diff --git a/Attribute/SubscribedService.php b/Attribute/SubscribedService.php index d98e1dfdb..f850b8401 100644 --- a/Attribute/SubscribedService.php +++ b/Attribute/SubscribedService.php @@ -11,15 +11,15 @@ namespace Symfony\Contracts\Service\Attribute; +use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; use Symfony\Contracts\Service\ServiceSubscriberInterface; -use Symfony\Contracts\Service\ServiceSubscriberTrait; /** * For use as the return value for {@see ServiceSubscriberInterface}. * * @example new SubscribedService('http_client', HttpClientInterface::class, false, new Target('githubApi')) * - * Use with {@see ServiceSubscriberTrait} to mark a method's return type + * Use with {@see ServiceMethodsSubscriberTrait} to mark a method's return type * as a subscribed service. * * @author Kevin Bond diff --git a/ServiceMethodsSubscriberTrait.php b/ServiceMethodsSubscriberTrait.php new file mode 100644 index 000000000..0d89d9f25 --- /dev/null +++ b/ServiceMethodsSubscriberTrait.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; +use Symfony\Contracts\Service\Attribute\SubscribedService; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services + * from methods that have the #[SubscribedService] attribute. + * + * Service ids are available as "ClassName::methodName" so that the implementation + * of subscriber methods can be just `return $this->container->get(__METHOD__);`. + * + * @author Kevin Bond + */ +trait ServiceMethodsSubscriberTrait +{ + protected ContainerInterface $container; + + public static function getSubscribedServices(): array + { + $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if (self::class !== $method->getDeclaringClass()->name) { + continue; + } + + if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { + continue; + } + + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + } + + if (!$returnType = $method->getReturnType()) { + throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + } + + /* @var SubscribedService $attribute */ + $attribute = $attribute->newInstance(); + $attribute->key ??= self::class.'::'.$method->name; + $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + $attribute->nullable = $returnType->allowsNull(); + + if ($attribute->attributes) { + $services[] = $attribute; + } else { + $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; + } + } + + return $services; + } + + #[Required] + public function setContainer(ContainerInterface $container): ?ContainerInterface + { + $ret = null; + if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { + $ret = parent::setContainer($container); + } + + $this->container = $container; + + return $ret; + } +} diff --git a/ServiceSubscriberTrait.php b/ServiceSubscriberTrait.php index f3b450cd6..cc3bc321a 100644 --- a/ServiceSubscriberTrait.php +++ b/ServiceSubscriberTrait.php @@ -15,17 +15,23 @@ use Symfony\Contracts\Service\Attribute\Required; use Symfony\Contracts\Service\Attribute\SubscribedService; +trigger_deprecation('symfony/contracts', 'v3.5', '"%s" is deprecated, use "ServiceMethodsSubscriberTrait" instead.', ServiceSubscriberTrait::class); + /** - * Implementation of ServiceSubscriberInterface that determines subscribed services from - * method return types. Service ids are available as "ClassName::methodName". + * Implementation of ServiceSubscriberInterface that determines subscribed services + * from methods that have the #[SubscribedService] attribute. + * + * Service ids are available as "ClassName::methodName" so that the implementation + * of subscriber methods can be just `return $this->container->get(__METHOD__);`. + * + * @property ContainerInterface $container * * @author Kevin Bond + * + * @deprecated since symfony/contracts v3.5, use ServiceMethodsSubscriberTrait instead */ trait ServiceSubscriberTrait { - /** @var ContainerInterface */ - protected $container; - public static function getSubscribedServices(): array { $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; diff --git a/composer.json b/composer.json index 061561c91..fc8674a72 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ ], "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" From 3366736f380f9c3b5fb477649bec65c6491f68dc Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 24 May 2024 11:59:23 +0200 Subject: [PATCH 4/9] use constructor property promotion --- ServiceLocatorTrait.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ServiceLocatorTrait.php b/ServiceLocatorTrait.php index b62ec3e53..a4f287326 100644 --- a/ServiceLocatorTrait.php +++ b/ServiceLocatorTrait.php @@ -26,16 +26,15 @@ class_exists(NotFoundExceptionInterface::class); */ trait ServiceLocatorTrait { - private array $factories; private array $loading = []; private array $providedTypes; /** * @param array $factories */ - public function __construct(array $factories) - { - $this->factories = $factories; + public function __construct( + private array $factories, + ) { } public function has(string $id): bool From bd2bf7e84b6318b0b761ca8b7379bd087e97742d Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 Jun 2024 17:52:34 +0200 Subject: [PATCH 5/9] Prefix all sprintf() calls --- ServiceLocatorTrait.php | 10 +++++----- ServiceMethodsSubscriberTrait.php | 4 ++-- ServiceSubscriberTrait.php | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ServiceLocatorTrait.php b/ServiceLocatorTrait.php index a4f287326..bbe454843 100644 --- a/ServiceLocatorTrait.php +++ b/ServiceLocatorTrait.php @@ -90,16 +90,16 @@ private function createNotFoundException(string $id): NotFoundExceptionInterface } else { $last = array_pop($alternatives); if ($alternatives) { - $message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); + $message = \sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); } else { - $message = sprintf('only knows about the "%s" service.', $last); + $message = \sprintf('only knows about the "%s" service.', $last); } } if ($this->loading) { - $message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); + $message = \sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); } else { - $message = sprintf('Service "%s" not found: the current service locator %s', $id, $message); + $message = \sprintf('Service "%s" not found: the current service locator %s', $id, $message); } return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface { @@ -108,7 +108,7 @@ private function createNotFoundException(string $id): NotFoundExceptionInterface private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface { - return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { + return new class(\sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { }; } } diff --git a/ServiceMethodsSubscriberTrait.php b/ServiceMethodsSubscriberTrait.php index 0d89d9f25..2c4c274f7 100644 --- a/ServiceMethodsSubscriberTrait.php +++ b/ServiceMethodsSubscriberTrait.php @@ -42,11 +42,11 @@ public static function getSubscribedServices(): array } if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { - throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + throw new \LogicException(\sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); } if (!$returnType = $method->getReturnType()) { - throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); } /* @var SubscribedService $attribute */ diff --git a/ServiceSubscriberTrait.php b/ServiceSubscriberTrait.php index cc3bc321a..f22a303b1 100644 --- a/ServiceSubscriberTrait.php +++ b/ServiceSubscriberTrait.php @@ -46,11 +46,11 @@ public static function getSubscribedServices(): array } if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { - throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + throw new \LogicException(\sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); } if (!$returnType = $method->getReturnType()) { - throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); } /* @var SubscribedService $attribute */ From a327b595fbecf4ec831598d4b5dd82e284725db4 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Tue, 25 Jun 2024 14:58:00 +0200 Subject: [PATCH 6/9] Add more precise types in reusable test cases --- Test/ServiceLocatorTestCase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Test/ServiceLocatorTestCase.php b/Test/ServiceLocatorTestCase.php index 65a3fe337..a6b87eb55 100644 --- a/Test/ServiceLocatorTestCase.php +++ b/Test/ServiceLocatorTestCase.php @@ -19,6 +19,9 @@ abstract class ServiceLocatorTestCase extends TestCase { + /** + * @param array $factories + */ protected function getServiceLocator(array $factories): ContainerInterface { return new class($factories) implements ContainerInterface { From fde0eff28709bb756317bd0083afa6e3ee469a14 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 5 Sep 2024 11:40:18 +0200 Subject: [PATCH 7/9] make test case classes compatible with PHPUnit 10+ --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fc8674a72..bc2e99a8f 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", From 7a42641cc734732b612bf68973ab9cc902d675b2 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 23 Sep 2024 12:42:15 +0200 Subject: [PATCH 8/9] Remove calls to getExpectedException() --- Test/ServiceLocatorTestCase.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Test/ServiceLocatorTestCase.php b/Test/ServiceLocatorTestCase.php index a6b87eb55..fdd5b2793 100644 --- a/Test/ServiceLocatorTestCase.php +++ b/Test/ServiceLocatorTestCase.php @@ -75,10 +75,8 @@ public function testThrowsOnUndefinedInternalService() 'foo' => function () use (&$locator) { return $locator->get('bar'); }, ]); - if (!$this->getExpectedException()) { - $this->expectException(NotFoundExceptionInterface::class); - $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); - } + $this->expectException(NotFoundExceptionInterface::class); + $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); $locator->get('foo'); } From f021b05a130d35510bd6b25fe9053c2a8a15d5d4 Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Fri, 25 Apr 2025 11:05:49 +0200 Subject: [PATCH 9/9] Fix ServiceMethodsSubscriberTrait for nullable service --- ServiceMethodsSubscriberTrait.php | 2 +- ServiceSubscriberTrait.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ServiceMethodsSubscriberTrait.php b/ServiceMethodsSubscriberTrait.php index 2c4c274f7..844be8907 100644 --- a/ServiceMethodsSubscriberTrait.php +++ b/ServiceMethodsSubscriberTrait.php @@ -53,7 +53,7 @@ public static function getSubscribedServices(): array $attribute = $attribute->newInstance(); $attribute->key ??= self::class.'::'.$method->name; $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; - $attribute->nullable = $returnType->allowsNull(); + $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull(); if ($attribute->attributes) { $services[] = $attribute; diff --git a/ServiceSubscriberTrait.php b/ServiceSubscriberTrait.php index f22a303b1..ed4cec044 100644 --- a/ServiceSubscriberTrait.php +++ b/ServiceSubscriberTrait.php @@ -57,7 +57,7 @@ public static function getSubscribedServices(): array $attribute = $attribute->newInstance(); $attribute->key ??= self::class.'::'.$method->name; $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; - $attribute->nullable = $returnType->allowsNull(); + $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull(); if ($attribute->attributes) { $services[] = $attribute;