diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 000000000..bec86a6cd --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,12 @@ +build: + environment: + php: + version: 7.4 + dependencies: + before: + - curl -sS https://getcomposer.org/installer | php -- --2 + - mv composer.phar composer + - export PATH="$PWD:$PATH" + tests: + override: + - php-scrutinizer-run \ No newline at end of file diff --git a/src/Di/Di.php b/src/Di/Di.php index d011ce35c..9b1f168f5 100644 --- a/src/Di/Di.php +++ b/src/Di/Di.php @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.7 + * @since 2.9.8 */ namespace Quantum\Di; @@ -39,47 +39,56 @@ class Di */ private static $container = []; + /** + * @var array + */ + private static $resolving = []; + /** * Loads dependency definitions */ public static function registerDependencies() { - foreach (self::coreDependencies() as $dependency) { - if (!self::isRegistered($dependency)) { - self::register($dependency); - } - } - - foreach (self::userDependencies() as $dependency) { - if (!self::isRegistered($dependency)) { - self::register($dependency); + foreach (self::coreDependencies() + self::userDependencies() as $abstract => $concrete) { + if (!self::isRegistered($abstract)) { + self::register($concrete, $abstract); } } } /** * Registers new dependency - * @param string $dependency - * @return bool + * @param string $concrete + * @param string|null $abstract + * @throws DiException */ - public static function register(string $dependency): bool + public static function register(string $concrete, ?string $abstract = null) { - if (!in_array($dependency, self::$dependencies) && class_exists($dependency)) { - self::$dependencies[] = $dependency; - return true; + $key = $abstract ?? $concrete; + + if (isset(self::$dependencies[$key])) { + throw DiException::dependencyAlreadyRegistered($key); + } + + if (!class_exists($concrete)) { + throw DiException::dependencyNotInstantiable($concrete); + } + + if ($abstract !== null && !class_exists($abstract) && !interface_exists($abstract)) { + throw DiException::invalidAbstractDependency($abstract); } - return false; + self::$dependencies[$key] = $concrete; } /** * Checks if a dependency registered - * @param string $dependency + * @param string $abstract * @return bool */ - public static function isRegistered(string $dependency): bool + public static function isRegistered(string $abstract): bool { - return in_array($dependency, self::$dependencies); + return isset(self::$dependencies[$abstract]); } /** @@ -140,47 +149,57 @@ public static function reset(): void { self::$dependencies = []; self::$container = []; + self::$resolving = []; } /** * Resolves the dependency - * @param string $dependency + * @param string $abstract * @param array $args * @param bool $singleton * @return mixed * @throws DiException * @throws ReflectionException */ - private static function resolve(string $dependency, array $args = [], bool $singleton = true) + private static function resolve(string $abstract, array $args = [], bool $singleton = true) { - if ($singleton) { - if (!isset(self::$container[$dependency])) { - self::$container[$dependency] = self::instantiate($dependency, $args); + self::checkCircularDependency($abstract); + + self::$resolving[$abstract] = true; + + try { + $concrete = self::$dependencies[$abstract]; + + if ($singleton) { + if (!isset(self::$container[$abstract])) { + self::$container[$abstract] = self::instantiate($concrete, $args); + } + return self::$container[$abstract]; } - return self::$container[$dependency]; + return self::instantiate($concrete, $args); + } finally { + unset(self::$resolving[$abstract]); } - - return self::instantiate($dependency, $args); } /** * Instantiates the dependency - * @param string $dependency + * @param string $concrete * @param array $args * @return mixed * @throws DiException * @throws ReflectionException */ - private static function instantiate(string $dependency, array $args = []) + private static function instantiate(string $concrete, array $args = []) { - $class = new ReflectionClass($dependency); + $class = new ReflectionClass($concrete); $constructor = $class->getConstructor(); $params = $constructor ? self::resolveParameters($constructor->getParameters(), $args) : []; - return new $dependency(...$params); + return new $concrete(...$params); } /** @@ -214,8 +233,10 @@ private static function resolveParameter(ReflectionParameter $param, ?array &$ar { $type = $param->getType() ? $param->getType()->getName() : null; - if ($type && self::instantiable($type)) { - return self::create($type); + $concrete = self::$dependencies[$type] ?? $type; + + if ($concrete && self::instantiable($concrete)) { + return self::create($concrete); } if ($type === 'array') { @@ -231,12 +252,25 @@ private static function resolveParameter(ReflectionParameter $param, ?array &$ar /** * Checks if the class is instantiable - * @param string $type + * @param string $class * @return bool */ - protected static function instantiable(string $type): bool + protected static function instantiable(string $class): bool { - return class_exists($type) && (new ReflectionClass($type))->isInstantiable(); + return class_exists($class) && (new ReflectionClass($class))->isInstantiable(); + } + + /** + * @param string $abstract + * @return void + * @throws DiException + */ + private static function checkCircularDependency(string $abstract): void + { + if (isset(self::$resolving[$abstract])) { + $chain = implode(' -> ', array_keys(self::$resolving)) . ' -> ' . $abstract; + throw DiException::circularDependency($chain); + } } /** @@ -261,9 +295,9 @@ private static function userDependencies(): array private static function coreDependencies(): array { return [ - \Quantum\Loader\Loader::class, - \Quantum\Http\Request::class, - \Quantum\Http\Response::class, + \Quantum\Loader\Loader::class => \Quantum\Loader\Loader::class, + \Quantum\Http\Request::class => \Quantum\Http\Request::class, + \Quantum\Http\Response::class => \Quantum\Http\Response::class, ]; } } diff --git a/src/Di/Exceptions/DiException.php b/src/Di/Exceptions/DiException.php index 9d2ec70f5..56a88b003 100644 --- a/src/Di/Exceptions/DiException.php +++ b/src/Di/Exceptions/DiException.php @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.7 + * @since 2.9.8 */ namespace Quantum\Di\Exceptions; @@ -30,4 +30,40 @@ public static function dependencyNotRegistered(string $name): DiException { return new self(t('exception.dependency_not_registered', $name), E_ERROR); } + + /** + * @param string $name + * @return DiException + */ + public static function dependencyAlreadyRegistered(string $name): DiException + { + return new self(t('exception.dependency_already_registered', $name), E_ERROR); + } + + /** + * @param string $name + * @return DiException + */ + public static function dependencyNotInstantiable(string $name): DiException + { + return new self(t('exception.dependency_not_instantiable', $name), E_ERROR); + } + + /** + * @param string $name + * @return DiException + */ + public static function invalidAbstractDependency(string $name): DiException + { + return new self(t('exception.invalid_abstract_dependency', $name), E_ERROR); + } + + /** + * @param string $chain + * @return DiException + */ + public static function circularDependency(string $chain): DiException + { + return new self("Circular dependency detected: $chain", 0); + } } diff --git a/tests/Unit/Di/DiTest.php b/tests/Unit/Di/DiTest.php index 06f01b787..4fefae2ac 100644 --- a/tests/Unit/Di/DiTest.php +++ b/tests/Unit/Di/DiTest.php @@ -2,6 +2,7 @@ namespace Quantum\Controllers { + use Quantum\Service\DummyServiceInterface; use Quantum\View\Factories\ViewFactory; use Quantum\Router\RouteController; use Quantum\Http\Request; @@ -13,20 +14,44 @@ public function index(Request $request, Response $response, ViewFactory $view) { // method body } + + public function handleService(DummyServiceInterface $service) + { + return $service; + } } } namespace Quantum\Service { - class DummyService extends QtService + interface DummyServiceInterface {} + + class DummyService extends QtService implements DummyServiceInterface + { + + } + + class CircularDependencyA { + public function __construct(CircularDependencyB $b) + { + } + } + class CircularDependencyB + { + public function __construct(CircularDependencyA $a) + { + } } } namespace Quantum\Tests\Unit\Di { + use Quantum\Service\DummyServiceInterface; use Quantum\Controllers\TestDiController; + use Quantum\Service\CircularDependencyA; + use Quantum\Service\CircularDependencyB; use Quantum\View\Factories\ViewFactory; use Quantum\Di\Exceptions\DiException; use Quantum\Tests\Unit\AppTestCase; @@ -44,6 +69,9 @@ class DiTest extends AppTestCase public function setUp(): void { parent::setUp(); + + Di::reset(); + Di::registerDependencies(); } public function testDiRegisterDependency() @@ -53,6 +81,32 @@ public function testDiRegisterDependency() $this->assertInstanceOf(Setup::class, Di::get(Setup::class)); } + public function testDiAttemptingToRegisterAlreadyRegisteredDependency(): void + { + $this->expectException(DiException::class); + + $this->expectExceptionMessage('exception.dependency_already_registered'); + + Di::register(Setup::class); + Di::register(Setup::class); + } + + public function testDiAttemptingToRegisterNonExistentClass(): void + { + $this->expectException(DiException::class); + $this->expectExceptionMessage('exception.dependency_not_instantiable'); + + Di::register('NonExistentClass'); + } + + public function testDiAttemptingToRegisterNonExistentAbstract(): void + { + $this->expectException(DiException::class); + $this->expectExceptionMessage('exception.invalid_abstract_dependency'); + + Di::register(DummyService::class, 'NonExistentInterface'); + } + public function testDiIsRegistered() { $this->assertFalse(Di::isRegistered(DummyService::class)); @@ -62,6 +116,15 @@ public function testDiIsRegistered() $this->assertTrue(Di::isRegistered(DummyService::class)); } + public function testDiAbstractToConcreteBinding() + { + Di::register(DummyService::class, DummyServiceInterface::class); + + $instance = Di::get(DummyServiceInterface::class); + + $this->assertInstanceOf(DummyService::class, $instance); + } + public function testDiGetCoreDependencies() { $this->assertInstanceOf(Loader::class, Di::get(Loader::class)); @@ -71,7 +134,7 @@ public function testDiGetCoreDependencies() $this->assertInstanceOf(Response::class, Di::get(Response::class)); } - public function testDiGetNotRegisteredDependency() + public function testDiAttemptingToGetNotRegisteredDependency() { $this->assertInstanceOf(Loader::class, Di::get(Loader::class)); @@ -82,6 +145,22 @@ public function testDiGetNotRegisteredDependency() Di::get(DiException::class); } + public function testDiCircularDependencyDetectedAtResolve(): void + { + $this->expectException(DiException::class); + + $this->expectExceptionMessage( + 'Circular dependency detected: ' . CircularDependencyA::class . + ' -> ' . CircularDependencyB::class . + ' -> ' . CircularDependencyA::class + ); + + Di::register(CircularDependencyA::class); + Di::register(CircularDependencyB::class); + + Di::create(CircularDependencyA::class); + } + public function testDiGetReturnsSingleton() { Di::register(DummyService::class); @@ -129,6 +208,19 @@ public function testDiAutowire() $this->assertInstanceOf(Response::class, $params[1]); } + public function testDiAutowireWithAbstract() + { + Di::register(DummyService::class, DummyServiceInterface::class); + + $controller = new TestDiController(); + + $params = Di::autowire([$controller, 'handleService']); + + $this->assertInstanceOf(DummyServiceInterface::class, $params[0]); + + $this->assertInstanceOf(DummyService::class, $params[0]); + } + public function testDiReset() { Di::register(DummyService::class);