Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #2: Allow other factories to get services to inject #3

Draft
wants to merge 2 commits into
base: 1.0
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/book/index.md

This file was deleted.

1 change: 1 addition & 0 deletions docs/book/index.md
5 changes: 4 additions & 1 deletion src/Factory/AttributedRepositoryFactory.php
Original file line number Diff line number Diff line change
@@ -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;
}
}

116 changes: 5 additions & 111 deletions src/Factory/AttributedServiceFactory.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
135 changes: 135 additions & 0 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

namespace Dot\DependencyInjection;

use ArrayAccess;
use Dot\DependencyInjection\Attribute\Inject;
use Dot\DependencyInjection\Exception\InvalidArgumentException;
use Dot\DependencyInjection\Exception\RuntimeException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;

use function array_map;
use function array_shift;
use function assert;
use function class_exists;
use function count;
use function explode;
use function in_array;
use function is_array;
use function sprintf;

class ServiceProvider
{
protected string $originalKey = '';

/**
* @param class-string $requestedName
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws ReflectionException
* @throws RuntimeException
*/
public function getServices(ContainerInterface $container, string $requestedName): array
{
return array_map(
fn ($service): mixed => $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;
}
}
2 changes: 1 addition & 1 deletion test/ConfigProviderTest.php
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@

class ConfigProviderTest extends TestCase
{
protected array $config;
protected array $config = [];

protected function setup(): void
{
56 changes: 28 additions & 28 deletions test/Factory/AttributedServiceFactoryTest.php
Original file line number Diff line number Diff line change
@@ -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
{