Skip to content

Commit

Permalink
feat: 🔖 add support for dynamic class resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
lombervid committed Jul 27, 2023
1 parent e8d068b commit 6280466
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 5 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Changelog

## [Unreleased]
## [0.7.0] - 2023-07-26

### Added

- Add support for dynamic class resolution
- Auto inject container reference into container

## [0.6.0] - 2023-07-25
Expand Down Expand Up @@ -76,7 +77,8 @@

- Changed `EntryNotFoundException` parent from `\InvalidArgumentException` to `NotFoundException`

[Unreleased]: https://github.com/phetit/dependency-injection/compare/v0.6.0...main
[Unreleased]: https://github.com/phetit/dependency-injection/compare/v0.7.0...main
[0.7.0]: https://github.com/phetit/dependency-injection/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/phetit/dependency-injection/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/phetit/dependency-injection/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/phetit/dependency-injection/compare/v0.3.0...v0.4.0
Expand Down
123 changes: 123 additions & 0 deletions src/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@
namespace Phetit\DependencyInjection;

use Closure;
use Phetit\DependencyInjection\Exception\Dependency\InvalidBuiltinTypeException;
use Phetit\DependencyInjection\Exception\Dependency\InvalidIntersectionTypeException;
use Phetit\DependencyInjection\Exception\Dependency\InvalidMissingTypeException;
use Phetit\DependencyInjection\Exception\Dependency\InvalidUnionTypeException;
use Phetit\DependencyInjection\Exception\EntryNotFoundException;
use Phetit\DependencyInjection\Resolver\FactoryServiceResolver;
use Phetit\DependencyInjection\Resolver\ServiceResolver;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionIntersectionType;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;

class ContainerBuilder extends Container
{
Expand All @@ -29,4 +40,116 @@ public function factory(string $id, Closure $resolver): static

return $this;
}

public function has(string $id): bool
{
if (parent::has($id)) {
return true;
}

return $this->isResolvable($id);
}

public function get(string $id): mixed
{
if (! $this->has($id)) {
throw new EntryNotFoundException();
}

if (parent::has($id)) {
return parent::get($id);
}

return $this->resolveClass($id);
}

protected function resolveClass(string $id): mixed
{
$reflectionClass = new ReflectionClass($id);

$constructor = $reflectionClass->getConstructor();

if ($constructor === null) {
return $reflectionClass->newInstance();
}

$parameters = $constructor->getParameters();

if (count($parameters) === 0) {
return $reflectionClass->newInstance();
}

$dependencies = $this->resolveDependencies($id, $parameters);

return $reflectionClass->newInstance(...$dependencies);
}

/**
* Resolves dynamic class dependencies
*
* @param string $id Class identifier
* @param ReflectionParameter[] $parameters
*
* @return mixed[]
*/
protected function resolveDependencies(string $id, array $parameters): array
{
$dependencies = [];

foreach ($parameters as $parameter) {
$dependencies[] = $this->resolveDependency($id, $parameter);
}

return $dependencies;
}

/**
* Resolves dynamic class dependency
*
* @param string $id Class identifier
* @param ReflectionParameter $parameter
*
* @return mixed
*/
protected function resolveDependency(string $id, ReflectionParameter $parameter): mixed
{
$type = $parameter->getType();

if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}

if (! $parameter->hasType()) {
throw new InvalidMissingTypeException($id, $parameter->name);
}

if ($parameter->allowsNull()) {
return null;
}

if ($type instanceof ReflectionUnionType) {
throw new InvalidUnionTypeException($id, $parameter->name);
}

if ($type instanceof ReflectionIntersectionType) {
throw new InvalidIntersectionTypeException($id, $parameter->name);
}

if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) {
throw new InvalidBuiltinTypeException($id, $parameter->name);
}

return $this->get($type->getName());
}

protected function isResolvable(string $id): bool
{
try {
$class = new ReflectionClass($id);
} catch (ReflectionException) {
return false;
}

return $class->isInstantiable();
}
}
12 changes: 12 additions & 0 deletions src/Exception/Dependency/InvalidBuiltinTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Exception\Dependency;

/**
* This exception is thrown when a class's dependency fails to be resolved because builtin type
*/
class InvalidBuiltinTypeException extends InvalidTypeException
{
}
12 changes: 12 additions & 0 deletions src/Exception/Dependency/InvalidIntersectionTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Exception\Dependency;

/**
* This exception is thrown when a class's dependency fails to be resolved because intersection type
*/
class InvalidIntersectionTypeException extends InvalidTypeException
{
}
13 changes: 13 additions & 0 deletions src/Exception/Dependency/InvalidMissingTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Exception\Dependency;

/**
* This exception is thrown when a class's dependency fails to be resolved because missing type hint
*/
class InvalidMissingTypeException extends InvalidTypeException
{
protected string $formatMessage = 'Failed to resolve class "%s" because parameter "%s" has no type hint';
}
24 changes: 24 additions & 0 deletions src/Exception/Dependency/InvalidTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Exception\Dependency;

use Phetit\DependencyInjection\Exception\ContainerException;

/**
* This exception is thrown when a class's dependency fails to be resolved because invalid type
*/
abstract class InvalidTypeException extends ContainerException
{
protected string $formatMessage = 'Failed to resolve class "%s" because invalid type for parameter "%s"';

/**
* @param string $class Class trying to be resolved
* @param string $parameter Dependency trying to be resolved
*/
public function __construct(string $class, string $parameter)
{
parent::__construct(sprintf($this->formatMessage, $class, $parameter));
}
}
12 changes: 12 additions & 0 deletions src/Exception/Dependency/InvalidUnionTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Exception\Dependency;

/**
* This exception is thrown when a class's dependency fails to be resolved because union type
*/
class InvalidUnionTypeException extends InvalidTypeException
{
}
51 changes: 51 additions & 0 deletions tests/ContainerBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

use Phetit\DependencyInjection\ContainerBuilder;
use Phetit\DependencyInjection\Tests\Fixtures\Service;
use Phetit\DependencyInjection\Tests\Fixtures\ServiceWithDefaultValue;
use Phetit\DependencyInjection\Tests\Fixtures\ServiceWithDependency;
use Phetit\DependencyInjection\Tests\Fixtures\ServiceWithNullableDependency;
use Phetit\DependencyInjection\Tests\Fixtures\ServiceWithoutConstructor;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;

Expand Down Expand Up @@ -50,4 +54,51 @@ public function testShouldInjectAutoReference(): void
self::assertInstanceOf(ContainerBuilder::class, $container);
self::assertSame($builder, $container);
}

public function testDynamicClassResolution(): void
{
$builder = new ContainerBuilder();

$service = $builder->get(Service::class);

self::assertInstanceOf(Service::class, $service);
}

public function testDynamicClassResolutionWithoutConstructor(): void
{
$builder = new ContainerBuilder();

$service = $builder->get(ServiceWithoutConstructor::class);

self::assertInstanceOf(ServiceWithoutConstructor::class, $service);
}

public function testDynamicClassResolutionWithDependency(): void
{
$builder = new ContainerBuilder();

$service = $builder->get(ServiceWithDependency::class);

self::assertInstanceOf(ServiceWithDependency::class, $service);
}

public function testDynamicClassResolutionWithNullableDependency(): void
{
$builder = new ContainerBuilder();

$service = $builder->get(ServiceWithNullableDependency::class);

self::assertInstanceOf(ServiceWithNullableDependency::class, $service);
self::assertNull($service->service);
}

public function testDynamicClassResolutionWithDefaultValue(): void
{
$builder = new ContainerBuilder();

$service = $builder->get(ServiceWithDefaultValue::class);

self::assertInstanceOf(ServiceWithDefaultValue::class, $service);
self::assertInstanceOf(ServiceWithoutConstructor::class, $service->service);
}
}
3 changes: 0 additions & 3 deletions tests/Fixtures/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,4 @@

class Service
{
public function __construct(public int $value = 0)
{
}
}
14 changes: 14 additions & 0 deletions tests/Fixtures/ServiceWithDefaultValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Tests\Fixtures;

class ServiceWithDefaultValue
{
public function __construct(
public Service $service = new ServiceWithoutConstructor(),
public int $value = 0,
) {
}
}
12 changes: 12 additions & 0 deletions tests/Fixtures/ServiceWithDependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Tests\Fixtures;

class ServiceWithDependency
{
public function __construct(public Service $service)
{
}
}
12 changes: 12 additions & 0 deletions tests/Fixtures/ServiceWithNullableDependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Tests\Fixtures;

class ServiceWithNullableDependency
{
public function __construct(public ?Service $service)
{
}
}
9 changes: 9 additions & 0 deletions tests/Fixtures/ServiceWithoutConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Phetit\DependencyInjection\Tests\Fixtures;

class ServiceWithoutConstructor extends Service
{
}

0 comments on commit 6280466

Please sign in to comment.