├── CHANGELOG.md ├── README.md ├── Test ├── ServiceLocatorTest.php └── ServiceLocatorTestCase.php ├── ServiceCollectionInterface.php ├── Attribute ├── Required.php └── SubscribedService.php ├── LICENSE ├── ResetInterface.php ├── composer.json ├── ServiceProviderInterface.php ├── ServiceSubscriberInterface.php ├── ServiceMethodsSubscriberTrait.php ├── ServiceSubscriberTrait.php └── ServiceLocatorTrait.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | The changelog is maintained for all Symfony contracts at the following URL: 5 | https://github.com/symfony/contracts/blob/main/CHANGELOG.md 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Symfony Service Contracts 2 | ========================= 3 | 4 | A set of abstractions extracted out of the Symfony components. 5 | 6 | Can be used to build on semantics that the Symfony components proved useful and 7 | that already have battle tested implementations. 8 | 9 | See https://github.com/symfony/contracts/blob/main/README.md for more information. 10 | -------------------------------------------------------------------------------- /Test/ServiceLocatorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service\Test; 13 | 14 | /** 15 | * @deprecated since PHPUnit 9.6 16 | */ 17 | class ServiceLocatorTest extends ServiceLocatorTestCase 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /ServiceCollectionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service; 13 | 14 | /** 15 | * A ServiceProviderInterface that is also countable and iterable. 16 | * 17 | * @author Kevin Bond 18 | * 19 | * @template-covariant T of mixed 20 | * 21 | * @extends ServiceProviderInterface 22 | * @extends \IteratorAggregate 23 | */ 24 | interface ServiceCollectionInterface extends ServiceProviderInterface, \Countable, \IteratorAggregate 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /Attribute/Required.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service\Attribute; 13 | 14 | /** 15 | * A required dependency. 16 | * 17 | * This attribute indicates that a property holds a required dependency. The annotated property or method should be 18 | * considered during the instantiation process of the containing class. 19 | * 20 | * @author Alexander M. Turek 21 | */ 22 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 23 | final class Required 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /ResetInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service; 13 | 14 | /** 15 | * Provides a way to reset an object to its initial state. 16 | * 17 | * When calling the "reset()" method on an object, it should be put back to its 18 | * initial state. This usually means clearing any internal buffers and forwarding 19 | * the call to internal dependencies. All properties of the object should be put 20 | * back to the same state it had when it was first ready to use. 21 | * 22 | * This method could be called, for example, to recycle objects that are used as 23 | * services, so that they can be used to handle several requests in the same 24 | * process loop (note that we advise making your services stateless instead of 25 | * implementing this interface when possible.) 26 | */ 27 | interface ResetInterface 28 | { 29 | /** 30 | * @return void 31 | */ 32 | public function reset(); 33 | } 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/service-contracts", 3 | "type": "library", 4 | "description": "Generic abstractions related to writing services", 5 | "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Nicolas Grekas", 11 | "email": "p@tchwork.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.1", 20 | "psr/container": "^1.1|^2.0", 21 | "symfony/deprecation-contracts": "^2.5|^3" 22 | }, 23 | "conflict": { 24 | "ext-psr": "<1.1|>=2" 25 | }, 26 | "autoload": { 27 | "psr-4": { "Symfony\\Contracts\\Service\\": "" }, 28 | "exclude-from-classmap": [ 29 | "/Test/" 30 | ] 31 | }, 32 | "minimum-stability": "dev", 33 | "extra": { 34 | "branch-alias": { 35 | "dev-main": "3.6-dev" 36 | }, 37 | "thanks": { 38 | "name": "symfony/contracts", 39 | "url": "https://github.com/symfony/contracts" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ServiceProviderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service; 13 | 14 | use Psr\Container\ContainerInterface; 15 | 16 | /** 17 | * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container. 18 | * 19 | * @author Nicolas Grekas 20 | * @author Mateusz Sip 21 | * 22 | * @template-covariant T of mixed 23 | */ 24 | interface ServiceProviderInterface extends ContainerInterface 25 | { 26 | /** 27 | * @return T 28 | */ 29 | public function get(string $id): mixed; 30 | 31 | public function has(string $id): bool; 32 | 33 | /** 34 | * Returns an associative array of service types keyed by the identifiers provided by the current container. 35 | * 36 | * Examples: 37 | * 38 | * * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface 39 | * * ['foo' => '?'] means the container provides service name "foo" of unspecified type 40 | * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null 41 | * 42 | * @return array The provided service types, keyed by service names 43 | */ 44 | public function getProvidedServices(): array; 45 | } 46 | -------------------------------------------------------------------------------- /Attribute/SubscribedService.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service\Attribute; 13 | 14 | use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; 15 | use Symfony\Contracts\Service\ServiceSubscriberInterface; 16 | 17 | /** 18 | * For use as the return value for {@see ServiceSubscriberInterface}. 19 | * 20 | * @example new SubscribedService('http_client', HttpClientInterface::class, false, new Target('githubApi')) 21 | * 22 | * Use with {@see ServiceMethodsSubscriberTrait} to mark a method's return type 23 | * as a subscribed service. 24 | * 25 | * @author Kevin Bond 26 | */ 27 | #[\Attribute(\Attribute::TARGET_METHOD)] 28 | final class SubscribedService 29 | { 30 | /** @var object[] */ 31 | public array $attributes; 32 | 33 | /** 34 | * @param string|null $key The key to use for the service 35 | * @param class-string|null $type The service class 36 | * @param bool $nullable Whether the service is optional 37 | * @param object|object[] $attributes One or more dependency injection attributes to use 38 | */ 39 | public function __construct( 40 | public ?string $key = null, 41 | public ?string $type = null, 42 | public bool $nullable = false, 43 | array|object $attributes = [], 44 | ) { 45 | $this->attributes = \is_array($attributes) ? $attributes : [$attributes]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ServiceSubscriberInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service; 13 | 14 | use Symfony\Contracts\Service\Attribute\SubscribedService; 15 | 16 | /** 17 | * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. 18 | * 19 | * The getSubscribedServices method returns an array of service types required by such instances, 20 | * optionally keyed by the service names used internally. Service types that start with an interrogation 21 | * mark "?" are optional, while the other ones are mandatory service dependencies. 22 | * 23 | * The injected service locators SHOULD NOT allow access to any other services not specified by the method. 24 | * 25 | * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. 26 | * This interface does not dictate any injection method for these service locators, although constructor 27 | * injection is recommended. 28 | * 29 | * @author Nicolas Grekas 30 | */ 31 | interface ServiceSubscriberInterface 32 | { 33 | /** 34 | * Returns an array of service types (or {@see SubscribedService} objects) required 35 | * by such instances, optionally keyed by the service names used internally. 36 | * 37 | * For mandatory dependencies: 38 | * 39 | * * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name 40 | * internally to fetch a service which must implement Psr\Log\LoggerInterface. 41 | * * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name 42 | * internally to fetch an iterable of Psr\Log\LoggerInterface instances. 43 | * * ['Psr\Log\LoggerInterface'] is a shortcut for 44 | * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface'] 45 | * 46 | * otherwise: 47 | * 48 | * * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency 49 | * * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency 50 | * * ['?Psr\Log\LoggerInterface'] is a shortcut for 51 | * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface'] 52 | * 53 | * additionally, an array of {@see SubscribedService}'s can be returned: 54 | * 55 | * * [new SubscribedService('logger', Psr\Log\LoggerInterface::class)] 56 | * * [new SubscribedService(type: Psr\Log\LoggerInterface::class, nullable: true)] 57 | * * [new SubscribedService('http_client', HttpClientInterface::class, attributes: new Target('githubApi'))] 58 | * 59 | * @return string[]|SubscribedService[] The required service types, optionally keyed by service names 60 | */ 61 | public static function getSubscribedServices(): array; 62 | } 63 | -------------------------------------------------------------------------------- /Test/ServiceLocatorTestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service\Test; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use Psr\Container\ContainerExceptionInterface; 16 | use Psr\Container\ContainerInterface; 17 | use Psr\Container\NotFoundExceptionInterface; 18 | use Symfony\Contracts\Service\ServiceLocatorTrait; 19 | 20 | abstract class ServiceLocatorTestCase extends TestCase 21 | { 22 | /** 23 | * @param array $factories 24 | */ 25 | protected function getServiceLocator(array $factories): ContainerInterface 26 | { 27 | return new class($factories) implements ContainerInterface { 28 | use ServiceLocatorTrait; 29 | }; 30 | } 31 | 32 | public function testHas() 33 | { 34 | $locator = $this->getServiceLocator([ 35 | 'foo' => fn () => 'bar', 36 | 'bar' => fn () => 'baz', 37 | fn () => 'dummy', 38 | ]); 39 | 40 | $this->assertTrue($locator->has('foo')); 41 | $this->assertTrue($locator->has('bar')); 42 | $this->assertFalse($locator->has('dummy')); 43 | } 44 | 45 | public function testGet() 46 | { 47 | $locator = $this->getServiceLocator([ 48 | 'foo' => fn () => 'bar', 49 | 'bar' => fn () => 'baz', 50 | ]); 51 | 52 | $this->assertSame('bar', $locator->get('foo')); 53 | $this->assertSame('baz', $locator->get('bar')); 54 | } 55 | 56 | public function testGetDoesNotMemoize() 57 | { 58 | $i = 0; 59 | $locator = $this->getServiceLocator([ 60 | 'foo' => function () use (&$i) { 61 | ++$i; 62 | 63 | return 'bar'; 64 | }, 65 | ]); 66 | 67 | $this->assertSame('bar', $locator->get('foo')); 68 | $this->assertSame('bar', $locator->get('foo')); 69 | $this->assertSame(2, $i); 70 | } 71 | 72 | public function testThrowsOnUndefinedInternalService() 73 | { 74 | $locator = $this->getServiceLocator([ 75 | 'foo' => function () use (&$locator) { return $locator->get('bar'); }, 76 | ]); 77 | 78 | $this->expectException(NotFoundExceptionInterface::class); 79 | $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); 80 | 81 | $locator->get('foo'); 82 | } 83 | 84 | public function testThrowsOnCircularReference() 85 | { 86 | $locator = $this->getServiceLocator([ 87 | 'foo' => function () use (&$locator) { return $locator->get('bar'); }, 88 | 'bar' => function () use (&$locator) { return $locator->get('baz'); }, 89 | 'baz' => function () use (&$locator) { return $locator->get('bar'); }, 90 | ]); 91 | 92 | $this->expectException(ContainerExceptionInterface::class); 93 | $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); 94 | 95 | $locator->get('foo'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ServiceMethodsSubscriberTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Symfony\Contracts\Service\Attribute\Required; 16 | use Symfony\Contracts\Service\Attribute\SubscribedService; 17 | 18 | /** 19 | * Implementation of ServiceSubscriberInterface that determines subscribed services 20 | * from methods that have the #[SubscribedService] attribute. 21 | * 22 | * Service ids are available as "ClassName::methodName" so that the implementation 23 | * of subscriber methods can be just `return $this->container->get(__METHOD__);`. 24 | * 25 | * @author Kevin Bond 26 | */ 27 | trait ServiceMethodsSubscriberTrait 28 | { 29 | protected ContainerInterface $container; 30 | 31 | public static function getSubscribedServices(): array 32 | { 33 | $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; 34 | 35 | foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { 36 | if (self::class !== $method->getDeclaringClass()->name) { 37 | continue; 38 | } 39 | 40 | if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { 41 | continue; 42 | } 43 | 44 | if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { 45 | 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)); 46 | } 47 | 48 | if (!$returnType = $method->getReturnType()) { 49 | throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); 50 | } 51 | 52 | /* @var SubscribedService $attribute */ 53 | $attribute = $attribute->newInstance(); 54 | $attribute->key ??= self::class.'::'.$method->name; 55 | $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; 56 | $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull(); 57 | 58 | if ($attribute->attributes) { 59 | $services[] = $attribute; 60 | } else { 61 | $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; 62 | } 63 | } 64 | 65 | return $services; 66 | } 67 | 68 | #[Required] 69 | public function setContainer(ContainerInterface $container): ?ContainerInterface 70 | { 71 | $ret = null; 72 | if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { 73 | $ret = parent::setContainer($container); 74 | } 75 | 76 | $this->container = $container; 77 | 78 | return $ret; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ServiceSubscriberTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Symfony\Contracts\Service\Attribute\Required; 16 | use Symfony\Contracts\Service\Attribute\SubscribedService; 17 | 18 | trigger_deprecation('symfony/contracts', 'v3.5', '"%s" is deprecated, use "ServiceMethodsSubscriberTrait" instead.', ServiceSubscriberTrait::class); 19 | 20 | /** 21 | * Implementation of ServiceSubscriberInterface that determines subscribed services 22 | * from methods that have the #[SubscribedService] attribute. 23 | * 24 | * Service ids are available as "ClassName::methodName" so that the implementation 25 | * of subscriber methods can be just `return $this->container->get(__METHOD__);`. 26 | * 27 | * @property ContainerInterface $container 28 | * 29 | * @author Kevin Bond 30 | * 31 | * @deprecated since symfony/contracts v3.5, use ServiceMethodsSubscriberTrait instead 32 | */ 33 | trait ServiceSubscriberTrait 34 | { 35 | public static function getSubscribedServices(): array 36 | { 37 | $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; 38 | 39 | foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { 40 | if (self::class !== $method->getDeclaringClass()->name) { 41 | continue; 42 | } 43 | 44 | if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { 45 | continue; 46 | } 47 | 48 | if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { 49 | 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)); 50 | } 51 | 52 | if (!$returnType = $method->getReturnType()) { 53 | throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); 54 | } 55 | 56 | /** @var SubscribedService $attribute */ 57 | $attribute = $attribute->newInstance(); 58 | $attribute->key ??= self::class.'::'.$method->name; 59 | $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; 60 | $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull(); 61 | 62 | if ($attribute->attributes) { 63 | $services[] = $attribute; 64 | } else { 65 | $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; 66 | } 67 | } 68 | 69 | return $services; 70 | } 71 | 72 | #[Required] 73 | public function setContainer(ContainerInterface $container): ?ContainerInterface 74 | { 75 | $ret = null; 76 | if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { 77 | $ret = parent::setContainer($container); 78 | } 79 | 80 | $this->container = $container; 81 | 82 | return $ret; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ServiceLocatorTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Contracts\Service; 13 | 14 | use Psr\Container\ContainerExceptionInterface; 15 | use Psr\Container\NotFoundExceptionInterface; 16 | 17 | // Help opcache.preload discover always-needed symbols 18 | class_exists(ContainerExceptionInterface::class); 19 | class_exists(NotFoundExceptionInterface::class); 20 | 21 | /** 22 | * A trait to help implement ServiceProviderInterface. 23 | * 24 | * @author Robin Chalas 25 | * @author Nicolas Grekas 26 | */ 27 | trait ServiceLocatorTrait 28 | { 29 | private array $loading = []; 30 | private array $providedTypes; 31 | 32 | /** 33 | * @param array $factories 34 | */ 35 | public function __construct( 36 | private array $factories, 37 | ) { 38 | } 39 | 40 | public function has(string $id): bool 41 | { 42 | return isset($this->factories[$id]); 43 | } 44 | 45 | public function get(string $id): mixed 46 | { 47 | if (!isset($this->factories[$id])) { 48 | throw $this->createNotFoundException($id); 49 | } 50 | 51 | if (isset($this->loading[$id])) { 52 | $ids = array_values($this->loading); 53 | $ids = \array_slice($this->loading, array_search($id, $ids)); 54 | $ids[] = $id; 55 | 56 | throw $this->createCircularReferenceException($id, $ids); 57 | } 58 | 59 | $this->loading[$id] = $id; 60 | try { 61 | return $this->factories[$id]($this); 62 | } finally { 63 | unset($this->loading[$id]); 64 | } 65 | } 66 | 67 | public function getProvidedServices(): array 68 | { 69 | if (!isset($this->providedTypes)) { 70 | $this->providedTypes = []; 71 | 72 | foreach ($this->factories as $name => $factory) { 73 | if (!\is_callable($factory)) { 74 | $this->providedTypes[$name] = '?'; 75 | } else { 76 | $type = (new \ReflectionFunction($factory))->getReturnType(); 77 | 78 | $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?'; 79 | } 80 | } 81 | } 82 | 83 | return $this->providedTypes; 84 | } 85 | 86 | private function createNotFoundException(string $id): NotFoundExceptionInterface 87 | { 88 | if (!$alternatives = array_keys($this->factories)) { 89 | $message = 'is empty...'; 90 | } else { 91 | $last = array_pop($alternatives); 92 | if ($alternatives) { 93 | $message = \sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); 94 | } else { 95 | $message = \sprintf('only knows about the "%s" service.', $last); 96 | } 97 | } 98 | 99 | if ($this->loading) { 100 | $message = \sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); 101 | } else { 102 | $message = \sprintf('Service "%s" not found: the current service locator %s', $id, $message); 103 | } 104 | 105 | return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface { 106 | }; 107 | } 108 | 109 | private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface 110 | { 111 | return new class(\sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { 112 | }; 113 | } 114 | } 115 | --------------------------------------------------------------------------------