From 8b0f89f3558b68a4b3f176040ea9a1185417e941 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Mon, 6 May 2024 14:43:00 +0200 Subject: [PATCH] Container chain --- src/Container/Container.php | 2 +- src/Container/ContainerLog.php | 25 ----- src/Container/Context.php | 84 ---------------- src/Container/Dependency.php | 96 +++++++++++++------ src/Container/DependencyChain.php | 61 ++++++++++++ .../Exceptions/CannotAutowireException.php | 24 ++--- .../CannotInstantiateDependencyException.php | 30 +++--- .../CircularDependencyException.php | 45 +++++---- src/Container/GenericContainer.php | 86 +++++++++++------ src/Container/InMemoryContainerLog.php | 78 --------------- tests/Unit/Container/ContainerTest.php | 19 ++++ .../CannotAutowireExceptionTest.php | 14 ++- .../CircularDependencyExceptionTest.php | 32 ++++--- .../Fixtures/CircularWithInitializerA.php | 13 +++ .../Fixtures/CircularWithInitializerB.php | 13 +++ .../CircularWithInitializerBInitializer.php | 16 ++++ .../Fixtures/CircularWithInitializerC.php | 13 +++ 17 files changed, 341 insertions(+), 310 deletions(-) delete mode 100644 src/Container/ContainerLog.php delete mode 100644 src/Container/Context.php create mode 100644 src/Container/DependencyChain.php delete mode 100644 src/Container/InMemoryContainerLog.php create mode 100644 tests/Unit/Container/Fixtures/CircularWithInitializerA.php create mode 100644 tests/Unit/Container/Fixtures/CircularWithInitializerB.php create mode 100644 tests/Unit/Container/Fixtures/CircularWithInitializerBInitializer.php create mode 100644 tests/Unit/Container/Fixtures/CircularWithInitializerC.php diff --git a/src/Container/Container.php b/src/Container/Container.php index bdb5d86..a6c2143 100644 --- a/src/Container/Container.php +++ b/src/Container/Container.php @@ -10,7 +10,7 @@ interface Container { public function register(string $className, callable $definition): self; - public function singleton(string $className, callable $definition): self; + public function singleton(string $className, object|callable $definition): self; public function config(object $config): self; diff --git a/src/Container/ContainerLog.php b/src/Container/ContainerLog.php deleted file mode 100644 index 497e69f..0000000 --- a/src/Container/ContainerLog.php +++ /dev/null @@ -1,25 +0,0 @@ -dependencies[] = $dependency; - - return $this; - } - - public function currentDependency(): ?Dependency - { - return $this->dependencies[array_key_last($this->dependencies)] ?? null; - } - - public function getName(): string - { - return match($this->reflector::class) { - ReflectionClass::class => $this->reflector->getName(), - ReflectionMethod::class => $this->reflector->getDeclaringClass()->getName(), - ReflectionFunction::class => $this->reflector->getName() . ' in ' . $this->reflector->getFileName() . ':' . $this->reflector->getStartLine(), - }; - } - - public function getShortName(): string - { - return match($this->reflector::class) { - ReflectionClass::class => $this->reflector->getShortName(), - ReflectionMethod::class => $this->reflector->getDeclaringClass()->getShortName(), - ReflectionFunction::class => $this->reflector->getShortName() . ' in ' . $this->reflector->getFileName() . ':' . $this->reflector->getStartLine(), - }; - } - - public function __toString(): string - { - return match($this->reflector::class) { - ReflectionClass::class => $this->classToString(), - ReflectionMethod::class => $this->methodToString(), - ReflectionFunction::class => $this->functionToString(), - }; - } - - private function classToString(): string - { - return $this->reflector->getShortName(); - } - - private function methodToString(): string - { - return $this->reflector->getDeclaringClass()->getShortName() . '::' . $this->reflector->getName() . '(' . $this->dependenciesToString() . ')'; - } - - private function functionToString(): string - { - return $this->reflector->getShortName() . ' in ' . $this->reflector->getFileName() . ':' . $this->reflector->getStartLine() . '(' . $this->dependenciesToString() . ')'; - } - - private function dependenciesToString(): string - { - return implode( - ', ', - array_map( - fn (Dependency $dependency) => (string) $dependency, - $this->dependencies, - ) - ); - } -} diff --git a/src/Container/Dependency.php b/src/Container/Dependency.php index 3692c27..dc8a197 100644 --- a/src/Container/Dependency.php +++ b/src/Container/Dependency.php @@ -4,62 +4,72 @@ namespace Tempest\Container; +use Closure; use ReflectionClass; +use ReflectionFunction; use ReflectionIntersectionType; +use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; use ReflectionType; use ReflectionUnionType; +use Reflector; final readonly class Dependency { public function __construct( - public ReflectionParameter|ReflectionClass $reflector, + public Reflector|ReflectionType|Closure|string $dependency, ) { } - public function getId(): string + public function getName(): string { - return $this->typeToString($this->getType()); + return $this->resolveName($this->dependency); } - public function __toString(): string + public function getShortName(): string { - $typeToString = $this->typeToString($this->getType()); - $parts = explode('\\', $typeToString); - $typeToString = $parts[array_key_last($parts)]; - - return implode( - ' ', - array_filter([ - $typeToString, - '$' . $this->reflector->getName(), - ]), - ); + return $this->resolveShortName($this->dependency); } - private function getType(): string|ReflectionType + public function equals(self $other): bool { - return match($this->reflector::class) { - ReflectionParameter::class => $this->reflector->getType(), - ReflectionClass::class => $this->reflector->getName(), - }; + return $this->getName() === $other->getName(); } - private function typeToString(string|ReflectionType|null $type): ?string + private function resolveName(ReflectionType|Reflector|string|Closure $dependency): string { - if ($type === null) { - return null; + if (is_string($dependency)) { + return $dependency; } - if (is_string($type)) { - return $type; + return match($dependency::class) { + ReflectionFunction::class => $dependency->getName() . ' in ' . $dependency->getFileName() . ':' . $dependency->getStartLine(), + ReflectionClass::class => $dependency->getName(), + ReflectionMethod::class => $dependency->getDeclaringClass()->getName() . '::' . $dependency->getName(), + ReflectionParameter::class => $this->resolveName($dependency->getType()), + ReflectionNamedType::class => $dependency->getName(), + ReflectionIntersectionType::class => $this->intersectionTypeToString($dependency), + ReflectionUnionType::class => $this->unionTypeToString($dependency), + default => 'unknown', + }; + } + + private function resolveShortName(ReflectionType|Reflector|string|Closure $dependency): string + { + if (is_string($dependency)) { + return $dependency; } - return match($type::class) { - ReflectionIntersectionType::class => $this->intersectionTypeToString($type), - ReflectionNamedType::class => $type->getName(), - ReflectionUnionType::class => $this->unionTypeToString($type), + return match($dependency::class) { + ReflectionFunction::class => $dependency->getShortName() . ' in ' . $dependency->getFileName() . ':' . $dependency->getStartLine(), + ReflectionClass::class => $dependency->getShortName(), + ReflectionMethod::class => $this->reflectionMethodToShortString($dependency), + ReflectionParameter::class => $this->resolveShortName($dependency->getType()), + ReflectionNamedType::class => $this->reflectionNameTypeToShortString($dependency), + ReflectionIntersectionType::class => $this->intersectionTypeToString($dependency), + ReflectionUnionType::class => $this->unionTypeToString($dependency), + default => 'unknown', }; } @@ -68,7 +78,7 @@ private function intersectionTypeToString(ReflectionIntersectionType $type): str return implode( '&', array_map( - fn (ReflectionType $subType) => $this->typeToString($subType), + fn (ReflectionType $subType) => $this->resolveName($subType), $type->getTypes(), ), ); @@ -79,9 +89,33 @@ private function unionTypeToString(ReflectionUnionType $type): string return implode( '|', array_map( - fn (ReflectionType $subType) => $this->typeToString($subType), + fn (ReflectionType $subType) => $this->resolveName($subType), $type->getTypes(), ), ); } + + private function reflectionMethodToShortString(ReflectionMethod $method): string + { + $string = $method->getDeclaringClass()->getShortName() . '::' . $method->getName() . '('; + + $parameters = []; + + foreach ($method->getParameters() as $parameter) { + $parameters[] = $this->resolveShortName($parameter) . ' $' . $parameter->getName(); + } + + $string .= implode(', ', $parameters); + + $string .= ')'; + + return $string; + } + + private function reflectionNameTypeToShortString(ReflectionNamedType $type): string + { + $parts = explode('\\', $type->getName()); + + return $parts[array_key_last($parts)] ?? $type->getName(); + } } diff --git a/src/Container/DependencyChain.php b/src/Container/DependencyChain.php new file mode 100644 index 0000000..8f2a503 --- /dev/null +++ b/src/Container/DependencyChain.php @@ -0,0 +1,61 @@ +dependencies[$dependency->getName()])) { + throw new CircularDependencyException($this, $dependency); + } + + $this->dependencies[$dependency->getName()] = $dependency; + + return $this; + } + + public function first(): Dependency + { + return $this->dependencies[array_key_first($this->dependencies)]; + } + + public function last(): Dependency + { + return $this->dependencies[array_key_last($this->dependencies)]; + } + + /** @return \Tempest\Container\Dependency[] */ + public function all(): array + { + return $this->dependencies; + } + + public function getOrigin(): string + { + return $this->origin; + } + + public function clone(): self + { + return clone $this; + } +} diff --git a/src/Container/Exceptions/CannotAutowireException.php b/src/Container/Exceptions/CannotAutowireException.php index 14a6653..a24290e 100644 --- a/src/Container/Exceptions/CannotAutowireException.php +++ b/src/Container/Exceptions/CannotAutowireException.php @@ -5,43 +5,43 @@ namespace Tempest\Container\Exceptions; use Exception; -use Tempest\Container\ContainerLog; +use Tempest\Container\DependencyChain; final class CannotAutowireException extends Exception { - public function __construct(ContainerLog $containerLog) + public function __construct(DependencyChain $chain) { - $stack = $containerLog->getStack(); + $stack = $chain->all(); - $firstContext = $stack[array_key_first($stack)]; - $lastContext = $stack[array_key_last($stack)]; + $firstDependency = $chain->first(); + $lastDependency = $chain->last(); - $message = PHP_EOL . PHP_EOL . "Cannot autowire {$firstContext->getName()} because {$lastContext->getName()} cannot be resolved" . PHP_EOL; + $message = PHP_EOL . PHP_EOL . "Cannot autowire {$firstDependency->getName()} because {$lastDependency->getName()} cannot be resolved" . PHP_EOL; $i = 0; - foreach ($stack as $currentContext) { + foreach ($stack as $currentDependency) { $pipe = match ($i) { 0 => '┌──', count($stack) - 1 => '└──', default => '├──', }; - $message .= PHP_EOL . "\t{$pipe} " . $currentContext; + $message .= PHP_EOL . "\t{$pipe} " . $currentDependency->getShortName(); $i++; } - $currentDependency = $lastContext->currentDependency(); - $currentDependencyName = (string) $currentDependency; - $firstPart = explode($currentDependencyName, (string) $lastContext)[0] ?? null; + $currentDependency = $lastDependency; + $currentDependencyName = $currentDependency->getShortName(); + $firstPart = explode($currentDependencyName, $lastDependency->getShortName())[0] ?? null; $fillerSpaces = str_repeat(' ', strlen($firstPart) + 3); $fillerArrows = str_repeat('▒', strlen($currentDependencyName)); $message .= PHP_EOL . "\t {$fillerSpaces}{$fillerArrows}"; $message .= PHP_EOL . PHP_EOL; - $message .= "Originally called in {$containerLog->getOrigin()}"; + $message .= "Originally called in {$chain->getOrigin()}"; $message .= PHP_EOL; parent::__construct($message); diff --git a/src/Container/Exceptions/CannotInstantiateDependencyException.php b/src/Container/Exceptions/CannotInstantiateDependencyException.php index de9dc24..69a9c7c 100644 --- a/src/Container/Exceptions/CannotInstantiateDependencyException.php +++ b/src/Container/Exceptions/CannotInstantiateDependencyException.php @@ -6,15 +6,15 @@ use Exception; use ReflectionClass; -use Tempest\Container\ContainerLog; +use Tempest\Container\DependencyChain; final class CannotInstantiateDependencyException extends Exception { - public function __construct(ReflectionClass $class, ContainerLog $containerLog) + public function __construct(ReflectionClass $class, DependencyChain $chain) { $message = "Cannot resolve {$class->getName()} because it is not an instantiable class. Maybe it's missing an initializer class?" . PHP_EOL; - $stack = $containerLog->getStack(); + $stack = $chain->all(); if ($stack === []) { parent::__construct($message); @@ -22,11 +22,9 @@ public function __construct(ReflectionClass $class, ContainerLog $containerLog) return; } - $lastContext = $stack[array_key_last($stack)]; - $i = 0; - foreach ($stack as $currentContext) { + foreach ($stack as $currentDependency) { $pipe = match (true) { count($stack) > 1 && $i === 0 => '┌──', count($stack) > 1 && $i === count($stack) - 1 => '└──', @@ -34,21 +32,21 @@ public function __construct(ReflectionClass $class, ContainerLog $containerLog) default => '├──', }; - $message .= PHP_EOL . "\t{$pipe} " . $currentContext; + $message .= PHP_EOL . "\t{$pipe} " . $currentDependency->getShortName(); $i++; } - $currentDependency = $lastContext->currentDependency(); - $currentDependencyName = (string)$currentDependency; - $firstPart = explode($currentDependencyName, (string)$lastContext)[0] ?? null; - $fillerSpaces = str_repeat(' ', strlen($firstPart) + 3); - $fillerArrows = str_repeat('▒', strlen($currentDependencyName)); - $message .= PHP_EOL . "\t {$fillerSpaces}{$fillerArrows}"; - - $message .= PHP_EOL . PHP_EOL; + $lastDependency = $chain->last(); + // $currentDependencyName = $lastDependency->getShortName(); + // $firstPart = explode($currentDependencyName, (string)$lastDependency)[0] ?? null; + // $fillerSpaces = str_repeat(' ', strlen($firstPart) + 3); + // $fillerArrows = str_repeat('▒', strlen($currentDependencyName)); + // $message .= PHP_EOL . "\t {$fillerSpaces}{$fillerArrows}"; + // + // $message .= PHP_EOL . PHP_EOL; - $message .= "Originally called in {$containerLog->getOrigin()}"; + $message .= "Originally called in {$chain->getOrigin()}"; $message .= PHP_EOL; parent::__construct($message); diff --git a/src/Container/Exceptions/CircularDependencyException.php b/src/Container/Exceptions/CircularDependencyException.php index 187a986..7eba7b0 100644 --- a/src/Container/Exceptions/CircularDependencyException.php +++ b/src/Container/Exceptions/CircularDependencyException.php @@ -5,43 +5,50 @@ namespace Tempest\Container\Exceptions; use Exception; -use Tempest\Container\ContainerLog; -use Tempest\Container\Context; +use Tempest\Container\Dependency; +use Tempest\Container\DependencyChain; final class CircularDependencyException extends Exception { - public function __construct(ContainerLog $containerLog, Context $circularDependencyContext) + public function __construct(DependencyChain $chain, Dependency $circularDependency) { - $stack = $containerLog->getStack(); - $firstContext = $stack[array_key_first($stack)]; - $lastContext = $stack[array_key_last($stack)]; + $firstDependency = $chain->first(); - $message = PHP_EOL . PHP_EOL . "Cannot autowire {$firstContext->getName()} because it has a circular dependency on {$circularDependencyContext->getName()}:" . PHP_EOL; + $message = PHP_EOL . PHP_EOL . "Cannot autowire {$firstDependency->getName()} because it has a circular dependency on {$circularDependency->getName()}:" . PHP_EOL; - $hasSeenDependency = false; + $hasSeenCircularDependency = false; - foreach ($stack as $context) { - if ($context->getName() === $circularDependencyContext->getName()) { - $prefix = '┌─►'; - $hasSeenDependency = true; - } elseif ($hasSeenDependency) { + $stack = $chain->all(); + + foreach ($stack as $currentDependency) { + if ($hasSeenCircularDependency) { $prefix = '│ '; + } elseif ($currentDependency->equals($circularDependency)) { + $prefix = '┌─►'; + $hasSeenCircularDependency = true; } else { $prefix = ' '; } - $message .= PHP_EOL . "\t{$prefix} " . $context; + $message .= PHP_EOL . "\t{$prefix} " . $currentDependency->getShortName(); } - $circularDependencyName = $circularDependencyContext->getShortName(); - $firstPart = explode($circularDependencyName, (string) $lastContext)[0] ?? null; - $fillerLines = str_repeat('─', strlen($firstPart) + 3); - $fillerArrows = str_repeat('▒', strlen($circularDependencyName)); + $circularDependencyName = $circularDependency->getShortName(); + $lastDependencyName = $chain->last()->getShortName(); + $firstPart = explode($circularDependencyName, $lastDependencyName)[0] ?? null; + + if ($lastDependencyName === $firstPart) { + $fillerLines = str_repeat('─', 3); + } else { + $fillerLines = str_repeat('─', strlen($firstPart) + 3); + } + + $fillerArrows = str_repeat('▒', strlen($circularDependencyName)); $message .= PHP_EOL . "\t└{$fillerLines}{$fillerArrows}"; $message .= PHP_EOL . PHP_EOL; - $message .= "Originally called in {$containerLog->getOrigin()}"; + $message .= "Originally called in {$chain->getOrigin()}"; $message .= PHP_EOL; parent::__construct($message); diff --git a/src/Container/GenericContainer.php b/src/Container/GenericContainer.php index 546f746..e4150ac 100644 --- a/src/Container/GenericContainer.php +++ b/src/Container/GenericContainer.php @@ -4,6 +4,7 @@ namespace Tempest\Container; +use Closure; use ReflectionClass; use ReflectionFunction; use ReflectionIntersectionType; @@ -35,7 +36,7 @@ public function __construct( * @var class-string $dynamicInitializers */ private array $dynamicInitializers = [], - private readonly ContainerLog $log = new InMemoryContainerLog(), + private ?DependencyChain $chain = null, ) { } @@ -56,38 +57,34 @@ public function register(string $className, callable $definition): self return $this; } - public function singleton(string $className, callable $definition): self + public function singleton(string $className, object|callable $definition): self { - $this->definitions[$className] = function () use ($definition, $className) { - $instance = $definition($this); - - $this->singletons[$className] = $instance; - - return $instance; - }; - - unset($this->singletons[$className]); + $this->singletons[$className] = $definition; return $this; } public function config(object $config): self { - $this->singleton($config::class, fn () => $config); + $this->singleton($config::class, $config); return $this; } public function get(string $className, mixed ...$params): object { - $this->log->startResolving(); + $this->resolveChain(); + + $dependency = $this->resolve($className, ...$params); + + $this->stopChain(); - return $this->resolve($className, ...$params); + return $dependency; } public function call(string|object $object, string $methodName, ...$params): mixed { - $this->log->startResolving(); + $this->resolveChain(); $object = is_string($object) ? $this->get($object) : $object; @@ -95,6 +92,8 @@ public function call(string|object $object, string $methodName, ...$params): mix $parameters = $this->autowireDependencies($reflectionMethod, $params); + $this->stopChain(); + return $reflectionMethod->invokeArgs($object, $parameters); } @@ -133,25 +132,30 @@ private function resolve(string $className, mixed ...$params): object { // Check if the class has been registered as a singleton. if ($instance = $this->singletons[$className] ?? null) { - $this->log->addContext(new Context(new ReflectionClass($className))); + if ($instance instanceof Closure) { + $instance = $instance($this); + $this->singletons[$className] = $instance; + } + + $this->resolveChain()->add(new ReflectionClass($className)); return $instance; } // Check if a callable has been registered to resolve this class. if ($definition = $this->definitions[$className] ?? null) { - $this->log->addContext(new Context(new ReflectionFunction($definition))); + $this->resolveChain()->add(new ReflectionFunction($definition)); return $definition($this); } // Next we check if any of our default initializers can initialize this class. - // If there's an initializer, we don't keep track of the log anymore, - // since initializers are outside the container's responsibility. if ($initializer = $this->initializerFor($className)) { + $this->resolveChain()->add(new ReflectionClass($initializer)); + $object = match (true) { - $initializer instanceof Initializer => $initializer->initialize($this), - $initializer instanceof DynamicInitializer => $initializer->initialize($className, $this), + $initializer instanceof Initializer => $initializer->initialize($this->clone()), + $initializer instanceof DynamicInitializer => $initializer->initialize($className, $this->clone()), }; // Check whether the initializer's result should be registered as a singleton @@ -205,7 +209,7 @@ private function autowire(string $className, mixed ...$params): object $constructor = $reflectionClass->getConstructor(); if (! $reflectionClass->isInstantiable()) { - throw new CannotInstantiateDependencyException($reflectionClass, $this->log); + throw new CannotInstantiateDependencyException($reflectionClass, $this->chain); } return $constructor === null @@ -225,14 +229,14 @@ private function autowire(string $className, mixed ...$params): object */ private function autowireDependencies(ReflectionMethod $method, array $parameters = []): array { - $this->log->addContext(new Context($method)); + $this->resolveChain()->add($method); $dependencies = []; // Build the class by iterating through its // dependencies and resolving them. foreach ($method->getParameters() as $parameter) { - $dependencies[] = $this->autowireDependency( + $dependencies[] = $this->clone()->autowireDependency( parameter: $parameter, providedValue: $parameters[$parameter->getName()] ?? null, ); @@ -243,7 +247,7 @@ private function autowireDependencies(ReflectionMethod $method, array $parameter private function autowireDependency(ReflectionParameter $parameter, mixed $providedValue = null): mixed { - $this->log->addDependency(new Dependency($parameter)); + // $this->resolveChain()->add($parameter); $parameterType = $parameter->getType(); @@ -280,7 +284,7 @@ private function autowireDependency(ReflectionParameter $parameter, mixed $provi // At this point, there is nothing else we can do; we don't know // how to autowire this dependency. - throw $lastThrowable ?? new CannotAutowireException($this->log); + throw $lastThrowable ?? new CannotAutowireException($this->chain); } private function autowireObjectDependency(ReflectionNamedType $type, mixed $providedValue): mixed @@ -299,7 +303,7 @@ private function autowireObjectDependency(ReflectionNamedType $type, mixed $prov // At this point, there is nothing else we can do; we don't know // how to autowire this dependency. - throw new CannotAutowireException($this->log); + throw new CannotAutowireException($this->chain); } private function autowireBuiltinDependency(ReflectionParameter $parameter, mixed $providedValue): mixed @@ -334,6 +338,32 @@ private function autowireBuiltinDependency(ReflectionParameter $parameter, mixed // At this point, there is nothing else we can do; we don't know // how to autowire this dependency. - throw new CannotAutowireException($this->log); + throw new CannotAutowireException($this->chain); + } + + private function clone(): self + { + return clone $this; + } + + private function resolveChain(): DependencyChain + { + if ($this->chain === null) { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + $this->chain = new DependencyChain($trace[1]['file'] . ':' . $trace[1]['line']); + } + + return $this->chain; + } + + private function stopChain(): void + { + $this->chain = null; + } + + public function __clone(): void + { + $this->chain = $this->chain?->clone(); } } diff --git a/src/Container/InMemoryContainerLog.php b/src/Container/InMemoryContainerLog.php deleted file mode 100644 index 5185a43..0000000 --- a/src/Container/InMemoryContainerLog.php +++ /dev/null @@ -1,78 +0,0 @@ -origin = $trace[1]['file'] . ':' . $trace[1]['line']; - $this->stack = []; - - return $this; - } - - public function addContext(Context $context): ContainerLog - { - if (isset($this->stack[$context->getName()])) { - throw new CircularDependencyException($this, $context); - } - - $this->stack[$context->getName()] = $context; - - return $this; - } - - public function addDependency(Dependency $dependency): ContainerLog - { - if ($this->stack === []) { - $reflector = $dependency->reflector; - - if ($reflector instanceof ReflectionParameter) { - $reflector = $reflector->getDeclaringClass(); - } - - $this->addContext(new Context($reflector)); - } - - $this->currentContext()->addDependency($dependency); - - return $this; - } - - public function getStack(): array - { - return $this->stack; - } - - public function currentContext(): Context - { - return $this->stack[array_key_last($this->stack)] - ?? throw new Exception("No current context found. That shoudn't happen. Aidan probably wrote a bug somewhere."); - } - - public function currentDependency(): ?Dependency - { - return $this->currentContext()->currentDependency(); - } - - public function getOrigin(): string - { - return $this->origin; - } -} diff --git a/tests/Unit/Container/ContainerTest.php b/tests/Unit/Container/ContainerTest.php index 1d14e5a..d439781 100644 --- a/tests/Unit/Container/ContainerTest.php +++ b/tests/Unit/Container/ContainerTest.php @@ -5,10 +5,13 @@ namespace Tests\Tempest\Unit\Container; use PHPUnit\Framework\TestCase; +use Tempest\Container\Exceptions\CircularDependencyException; use Tempest\Container\GenericContainer; use Tests\Tempest\Unit\Container\Fixtures\BuiltinArrayClass; use Tests\Tempest\Unit\Container\Fixtures\BuiltinTypesWithDefaultsClass; use Tests\Tempest\Unit\Container\Fixtures\CallContainerObjectE; +use Tests\Tempest\Unit\Container\Fixtures\CircularWithInitializerA; +use Tests\Tempest\Unit\Container\Fixtures\CircularWithInitializerBInitializer; use Tests\Tempest\Unit\Container\Fixtures\ContainerObjectA; use Tests\Tempest\Unit\Container\Fixtures\ContainerObjectB; use Tests\Tempest\Unit\Container\Fixtures\ContainerObjectC; @@ -189,4 +192,20 @@ public function test_intersection_initializers() $this->assertInstanceOf(UnionImplementation::class, $a); $this->assertInstanceOf(UnionImplementation::class, $b); } + + public function test_circular_with_initializer_log(): void + { + $container = new GenericContainer(); + $container->addInitializer(CircularWithInitializerBInitializer::class); + + try { + $container->get(CircularWithInitializerA::class); + } catch (CircularDependencyException $e) { + $this->assertStringContainsString('CircularWithInitializerA', $e->getMessage()); + $this->assertStringContainsString('CircularWithInitializerB', $e->getMessage()); + $this->assertStringContainsString('CircularWithInitializerBInitializer', $e->getMessage()); + $this->assertStringContainsString('CircularWithInitializerC', $e->getMessage()); + $this->assertStringContainsString(__FILE__, $e->getMessage()); + } + } } diff --git a/tests/Unit/Container/Exceptions/CannotAutowireExceptionTest.php b/tests/Unit/Container/Exceptions/CannotAutowireExceptionTest.php index 57a40fd..3472449 100644 --- a/tests/Unit/Container/Exceptions/CannotAutowireExceptionTest.php +++ b/tests/Unit/Container/Exceptions/CannotAutowireExceptionTest.php @@ -24,12 +24,16 @@ public function test_autowire_without_exception() $container->get(AutowireA::class); } catch (CannotAutowireException $exception) { - $this->assertStringContainsString("Cannot autowire Tests\\Tempest\\Unit\\Container\\Fixtures\\AutowireA because Tests\\Tempest\\Unit\\Container\\Fixtures\\AutowireC cannot be resolved", $exception->getMessage()); + $this->assertStringContainsString('Cannot autowire Tests\Tempest\Unit\Container\Fixtures\AutowireA::__construct because Tests\Tempest\Unit\Container\Fixtures\AutowireC::__construct cannot be resolved', $exception->getMessage()); - $this->assertStringContainsString("┌── AutowireA::__construct(AutowireB \$b)", $exception->getMessage()); - $this->assertStringContainsString("├── AutowireB::__construct(AutowireC \$c)", $exception->getMessage()); - $this->assertStringContainsString("└── AutowireC::__construct(ContainerObjectA \$other, string \$unknown)", $exception->getMessage()); - $this->assertStringContainsString(" ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒", $exception->getMessage()); + $expected = <<<'TXT' + ┌── AutowireA::__construct(AutowireB $b) + ├── AutowireB::__construct(AutowireC $c) + └── AutowireC::__construct(ContainerObjectA $other, string $unknown) + ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ +TXT; + + $this->assertStringContainsString($expected, $exception->getMessage()); $this->assertStringContainsString("CannotAutowireExceptionTest.php:25", $exception->getMessage()); throw $exception; diff --git a/tests/Unit/Container/Exceptions/CircularDependencyExceptionTest.php b/tests/Unit/Container/Exceptions/CircularDependencyExceptionTest.php index 40a4700..5636924 100644 --- a/tests/Unit/Container/Exceptions/CircularDependencyExceptionTest.php +++ b/tests/Unit/Container/Exceptions/CircularDependencyExceptionTest.php @@ -25,12 +25,17 @@ public function test_circular_dependency_test() $container->get(CircularA::class); } catch (CircularDependencyException $exception) { - $this->assertStringContainsString("Cannot autowire Tests\\Tempest\\Unit\\Container\\Fixtures\\CircularA because it has a circular dependency on Tests\\Tempest\\Unit\\Container\\Fixtures\\CircularA", $exception->getMessage()); + $this->assertStringContainsString("Cannot autowire Tests\\Tempest\\Unit\\Container\\Fixtures\\CircularA::__construct because it has a circular dependency on Tests\\Tempest\\Unit\\Container\\Fixtures\\CircularA::__construct", $exception->getMessage()); + + $expected = <<<'TXT' + ┌─► CircularA::__construct(ContainerObjectA $other, CircularB $b) + │ CircularB::__construct(CircularC $c) + │ CircularC::__construct(ContainerObjectA $other, CircularA $a) + └───▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ +TXT; + + $this->assertStringContainsString($expected, $exception->getMessage()); - $this->assertStringContainsString("┌─► CircularA::__construct(ContainerObjectA \$other, CircularB \$b)", $exception->getMessage()); - $this->assertStringContainsString("│ CircularB::__construct(CircularC \$c)", $exception->getMessage()); - $this->assertStringContainsString("│ CircularC::__construct(ContainerObjectA \$other, CircularA \$a)", $exception->getMessage()); - $this->assertStringContainsString("└───────────────────────────────────────────────────▒▒▒▒▒▒▒▒▒", $exception->getMessage()); $this->assertStringContainsString("CircularDependencyExceptionTest.php:", $exception->getMessage()); throw $exception; @@ -46,12 +51,17 @@ public function test_circular_dependency_as_a_child_test() $container->get(CircularZ::class); } catch (CircularDependencyException $exception) { - $this->assertStringContainsString("Cannot autowire Tests\\Tempest\\Unit\\Container\\Fixtures\\CircularZ because it has a circular dependency on Tests\\Tempest\\Unit\\Container\\Fixtures\\CircularA", $exception->getMessage()); - $this->assertStringContainsString(" CircularZ::__construct(CircularA \$a)", $exception->getMessage()); - $this->assertStringContainsString("┌─► CircularA::__construct(ContainerObjectA \$other, CircularB \$b)", $exception->getMessage()); - $this->assertStringContainsString("│ CircularB::__construct(CircularC \$c)", $exception->getMessage()); - $this->assertStringContainsString("│ CircularC::__construct(ContainerObjectA \$other, CircularA \$a)", $exception->getMessage()); - $this->assertStringContainsString("└───────────────────────────────────────────────────▒▒▒▒▒▒▒▒▒", $exception->getMessage()); + $this->assertStringContainsString('Cannot autowire Tests\Tempest\Unit\Container\Fixtures\CircularZ::__construct because it has a circular dependency on Tests\Tempest\Unit\Container\Fixtures\CircularA::__construct:', $exception->getMessage()); + + $expected = <<<'TXT' + CircularZ::__construct(CircularA $a) + ┌─► CircularA::__construct(ContainerObjectA $other, CircularB $b) + │ CircularB::__construct(CircularC $c) + │ CircularC::__construct(ContainerObjectA $other, CircularA $a) + └───▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ +TXT; + + $this->assertStringContainsString($expected, $exception->getMessage()); throw $exception; } diff --git a/tests/Unit/Container/Fixtures/CircularWithInitializerA.php b/tests/Unit/Container/Fixtures/CircularWithInitializerA.php new file mode 100644 index 0000000..d1987d1 --- /dev/null +++ b/tests/Unit/Container/Fixtures/CircularWithInitializerA.php @@ -0,0 +1,13 @@ +get(CircularWithInitializerC::class)); + } +} diff --git a/tests/Unit/Container/Fixtures/CircularWithInitializerC.php b/tests/Unit/Container/Fixtures/CircularWithInitializerC.php new file mode 100644 index 0000000..28ba6a7 --- /dev/null +++ b/tests/Unit/Container/Fixtures/CircularWithInitializerC.php @@ -0,0 +1,13 @@ +