From b6b117373630b47e329e430b5dd36b6d94bafe6b Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 17 Oct 2024 10:02:03 +0300 Subject: [PATCH 1/2] Issue #2: Allow other factories to get services to inject Signed-off-by: alexmerlin --- src/Factory/AttributedRepositoryFactory.php | 5 +- src/Factory/AttributedServiceFactory.php | 116 +-------------- src/ServiceProvider.php | 135 ++++++++++++++++++ test/ConfigProviderTest.php | 2 +- test/Factory/AttributedServiceFactoryTest.php | 56 ++++---- 5 files changed, 173 insertions(+), 141 deletions(-) create mode 100644 src/ServiceProvider.php diff --git a/src/Factory/AttributedRepositoryFactory.php b/src/Factory/AttributedRepositoryFactory.php index d5c64ca..275f02b 100644 --- a/src/Factory/AttributedRepositoryFactory.php +++ b/src/Factory/AttributedRepositoryFactory.php @@ -13,6 +13,7 @@ use Psr\Container\NotFoundExceptionInterface; use ReflectionClass; +use function assert; use function class_exists; class AttributedRepositoryFactory @@ -54,7 +55,9 @@ protected function findEntityAttribute(ReflectionClass $reflectionClass): ?Entit $attributes = $reflectionClass->getAttributes(); foreach ($attributes as $attribute) { if ($attribute->getName() === Entity::class) { - return $attribute->newInstance(); + $instance = $attribute->newInstance(); + assert($instance instanceof Entity); + return $instance; } } diff --git a/src/Factory/AttributedServiceFactory.php b/src/Factory/AttributedServiceFactory.php index e0ce6ac..ee98992 100644 --- a/src/Factory/AttributedServiceFactory.php +++ b/src/Factory/AttributedServiceFactory.php @@ -4,137 +4,31 @@ namespace Dot\DependencyInjection\Factory; -use ArrayAccess; -use Dot\DependencyInjection\Attribute\Inject; -use Dot\DependencyInjection\Exception\InvalidArgumentException; use Dot\DependencyInjection\Exception\RuntimeException; +use Dot\DependencyInjection\ServiceProvider; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; -use ReflectionClass; -use ReflectionMethod; +use ReflectionException; -use function array_shift; use function class_exists; -use function count; -use function explode; -use function in_array; -use function is_array; -use function sprintf; class AttributedServiceFactory { - protected string $originalKey; - /** + * @param class-string $requestedName * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface + * @throws ReflectionException */ public function __invoke(ContainerInterface $container, string $requestedName): mixed - { - return $this->createObject($container, $requestedName); - } - - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - public function createObject(ContainerInterface $container, string $requestedName): mixed { if (! class_exists($requestedName)) { throw RuntimeException::classNotFound($requestedName); } - $constructor = (new ReflectionClass($requestedName))->getConstructor(); - if ($constructor === null) { - return new $requestedName(); - } - - $injectAttribute = $this->findInjectAttribute($constructor); - if (! $injectAttribute instanceof Inject) { - throw RuntimeException::attributeNotFound(Inject::class, $requestedName, static::class); - } - - if (in_array($requestedName, $injectAttribute->getServices(), true)) { - throw RuntimeException::recursiveInject($requestedName); - } - - $services = $this->getServicesToInject($container, $injectAttribute->getServices()); + $services = (new ServiceProvider())->getServices($container, $requestedName); return new $requestedName(...$services); } - - protected function findInjectAttribute(ReflectionMethod $constructor): ?Inject - { - $attributes = $constructor->getAttributes(); - foreach ($attributes as $attribute) { - if ($attribute->getName() === Inject::class) { - return $attribute->newInstance(); - } - } - - return null; - } - - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - protected function getServicesToInject(ContainerInterface $container, array $parameters): array - { - $services = []; - - foreach ($parameters as $parameter) { - $services[] = $this->getServiceToInject($container, $parameter); - } - - return $services; - } - - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - protected function getServiceToInject(ContainerInterface $container, string $serviceKey): mixed - { - $this->originalKey = $serviceKey; - - /** - * Even when dots are found, try to find a service with the full name. - * If it is not found, then assume dots are used to get part of an array service - */ - $parts = explode('.', $serviceKey); - if (count($parts) > 1 && ! $container->has($serviceKey)) { - $serviceKey = array_shift($parts); - } else { - $parts = []; - } - - if ($container->has($serviceKey)) { - $service = $container->get($serviceKey); - } elseif (class_exists($serviceKey)) { - $service = new $serviceKey(); - } else { - throw RuntimeException::classNotFound($serviceKey); - } - - return empty($parts) ? $service : $this->readKeysFromArray($parts, $service); - } - - protected function readKeysFromArray(array $keys, mixed $array): mixed - { - $key = array_shift($keys); - if (! isset($array[$key])) { - throw new InvalidArgumentException( - sprintf(InvalidArgumentException::MESSAGE_MISSING_KEY, $this->originalKey) - ); - } - - $value = $array[$key]; - if (! empty($keys) && (is_array($value) || $value instanceof ArrayAccess)) { - $value = $this->readKeysFromArray($keys, $value); - } - - return $value; - } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php new file mode 100644 index 0000000..cf91b90 --- /dev/null +++ b/src/ServiceProvider.php @@ -0,0 +1,135 @@ + $this->getServiceInstance($container, $service), + $this->getServicesToInject($requestedName) + ); + } + + /** + * @param class-string $requestedName + * @throws ReflectionException + * @throws RuntimeException + */ + protected function getServicesToInject(string $requestedName): array + { + $constructor = (new ReflectionClass($requestedName))->getConstructor(); + if ($constructor === null) { + return []; + } + + $injectAttribute = $this->findInjectAttribute($constructor); + if (! $injectAttribute instanceof Inject) { + return []; + } + + if (in_array($requestedName, $injectAttribute->getServices(), true)) { + throw RuntimeException::recursiveInject($requestedName); + } + + return $injectAttribute->getServices(); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws RuntimeException + */ + protected function getServiceInstance(ContainerInterface $container, string $serviceKey): mixed + { + $this->originalKey = $serviceKey; + + /** + * Even when dots are found, try to find a service with the full name. + * If it is not found, then assume dots are used to get part of an array service + */ + $parts = explode('.', $serviceKey); + if (count($parts) > 1 && ! $container->has($serviceKey)) { + $serviceKey = array_shift($parts); + } else { + $parts = []; + } + + if ($container->has($serviceKey)) { + $service = $container->get($serviceKey); + } elseif (class_exists($serviceKey)) { + $service = new $serviceKey(); + } else { + throw RuntimeException::classNotFound($serviceKey); + } + + return empty($parts) ? $service : $this->readKeysFromArray($parts, $service); + } + + /** + * @throws InvalidArgumentException + */ + protected function readKeysFromArray(array $keys, array|ArrayAccess $array): mixed + { + $key = array_shift($keys); + if (! isset($array[$key])) { + throw new InvalidArgumentException( + sprintf(InvalidArgumentException::MESSAGE_MISSING_KEY, $this->originalKey) + ); + } + + $value = $array[$key]; + if (! empty($keys) && (is_array($value) || $value instanceof ArrayAccess)) { + $value = $this->readKeysFromArray($keys, $value); + } + + return $value; + } + + protected function findInjectAttribute(ReflectionMethod $constructor): ?Inject + { + $attributes = $constructor->getAttributes(); + foreach ($attributes as $attribute) { + if ($attribute->getName() === Inject::class) { + $instance = $attribute->newInstance(); + assert($instance instanceof Inject); + return $instance; + } + } + + return null; + } +} diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php index 02365e6..95368d3 100644 --- a/test/ConfigProviderTest.php +++ b/test/ConfigProviderTest.php @@ -9,7 +9,7 @@ class ConfigProviderTest extends TestCase { - protected array $config; + protected array $config = []; protected function setup(): void { diff --git a/test/Factory/AttributedServiceFactoryTest.php b/test/Factory/AttributedServiceFactoryTest.php index c5686de..124f360 100644 --- a/test/Factory/AttributedServiceFactoryTest.php +++ b/test/Factory/AttributedServiceFactoryTest.php @@ -15,6 +15,7 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use ReflectionException; use function array_key_exists; use function sprintf; @@ -25,6 +26,8 @@ class AttributedServiceFactoryTest extends TestCase * @throws Exception * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface + * @throws ReflectionException + * @psalm-suppress ArgumentTypeCoercion */ public function testWillThrowExceptionIfClassNotFound(): void { @@ -44,34 +47,7 @@ public function testWillThrowExceptionIfClassNotFound(): void * @throws Exception * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface - */ - public function testWillThrowExceptionIfAttributeNotFound(): void - { - $container = $this->createMock(ContainerInterface::class); - - $subject = new class { - public function __construct() - { - } - }; - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage( - sprintf( - RuntimeException::MESSAGE_ATTRIBUTE_NOT_FOUND, - Inject::class, - $subject::class, - AttributedServiceFactory::class - ) - ); - - (new AttributedServiceFactory())($container, $subject::class); - } - - /** - * @throws Exception - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface + * @throws ReflectionException */ public function testWillThrowExceptionOnRecursiveInjection(): void { @@ -94,6 +70,7 @@ public function testWillThrowExceptionOnRecursiveInjection(): void * @throws ContainerExceptionInterface * @throws Exception * @throws NotFoundExceptionInterface + * @throws ReflectionException */ public function testWillThrowExceptionIfDottedServiceNotFound(): void { @@ -141,6 +118,7 @@ public function __construct(array $config = []) * @throws ContainerExceptionInterface * @throws Exception * @throws NotFoundExceptionInterface + * @throws ReflectionException */ public function testWillThrowExceptionIfDependencyNotFound(): void { @@ -166,6 +144,7 @@ public function __construct(mixed $test = null) * @throws ContainerExceptionInterface * @throws Exception * @throws NotFoundExceptionInterface + * @throws ReflectionException */ public function testWillCreateServiceIfNoConstructor(): void { @@ -178,10 +157,31 @@ public function testWillCreateServiceIfNoConstructor(): void $this->assertInstanceOf($subject::class, $service); } + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws ReflectionException + */ + public function testWillCreateServiceIfAttributeNotFound(): void + { + $container = $this->createMock(ContainerInterface::class); + + $subject = new class { + public function __construct() + { + } + }; + + $service = (new AttributedServiceFactory())($container, $subject::class); + $this->assertInstanceOf($subject::class, $service); + } + /** * @throws ContainerExceptionInterface * @throws Exception * @throws NotFoundExceptionInterface + * @throws ReflectionException */ public function testWillCreateService(): void { From 5a6fddb7775c3dcc46b385a4b53b5a5d91e92459 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 17 Oct 2024 10:05:40 +0300 Subject: [PATCH 2/2] Fixed docs/book/index.md Signed-off-by: alexmerlin --- docs/book/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 120000 docs/book/index.md diff --git a/docs/book/index.md b/docs/book/index.md deleted file mode 100644 index ae42a26..0000000 --- a/docs/book/index.md +++ /dev/null @@ -1 +0,0 @@ -../../README.md diff --git a/docs/book/index.md b/docs/book/index.md new file mode 120000 index 0000000..fe84005 --- /dev/null +++ b/docs/book/index.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file