diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index 8c16ba3..d140070 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -12,6 +12,7 @@ use Norvica\Container\Definition\Run; use Norvica\Container\Definition\Val; use Norvica\Container\Exception\ContainerException; +use Norvica\Container\Visitor; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr; @@ -53,49 +54,47 @@ final class Compiler { private readonly Definitions $definitions; private readonly Parser $parser; - private array $ast; - private array $map; - private array $postponed; + private readonly Visitor $visitor; + private array $body; + private array $hashes; public function __construct( Definitions $definitions, ) { $ids = array_keys($definitions->all()); $hashes = array_map('md5', $ids); - $this->map = array_combine($ids, $hashes); + $this->hashes = array_combine($ids, $hashes); $this->definitions = $definitions; - $this->postponed = []; $this->parser = (new ParserFactory())->createForNewestSupportedVersion(); + $this->visitor = new Visitor(); } public function compile(string $class = 'Container'): string { - $this->ast = $this->parser->parse(file_get_contents(__DIR__ . '/template.php')); -// echo (new \PhpParser\NodeDumper())->dump($this->ast)."\n";die; + $ast = $this->parser->parse(file_get_contents(__DIR__ . '/template.php')); - $this->ast[1]->name = new Identifier(name: $class); - $body = &$this->ast[1]->stmts; - $map = &$body[0]->consts[0]->value->items; + $ast[1]->name = new Identifier(name: $class); + $this->body = &$ast[1]->stmts; + $map = &$this->body[0]->consts[0]->value->items; - foreach ($this->map as $id => $hash) { - $definition = $this->definitions->get($id); - $body[] = $this->method($id, $hash, $this->definition($definition, $id)); - } + foreach ($this->hashes as $id => $hash) { + // skip entries processed by DFS + if (isset($this->body[$id])) { + continue; + } - while ($this->postponed) { - $id = array_shift($this->postponed); - $body[] = $this->method($id, $this->map[$id], $this->definition(new Obj($id), $id)); + $definition = $this->definitions->get($id); + $this->method($id, $hash, $definition); } - // $this->definition(new Obj($id), $id) - foreach ($this->map as $id => $hash) { + foreach ($this->hashes as $id => $hash) { $map[] = new ArrayItem( value: new String_(value: $hash), key: new String_(value: $id), ); } - return (new Standard())->prettyPrintFile($this->ast); + return (new Standard())->prettyPrintFile($ast); } private function definition( @@ -106,18 +105,6 @@ private function definition( return $this->scalar($definition); } - if (is_array($definition)) { - $items = []; - foreach ($definition as $key => $item) { - $items[] = new ArrayItem( - value: $this->definition($item), - key: is_int($key) ? new Int_(value: $key): new String_(value: $key), - ); - } - - return new Array_(items: $items); - } - if ($definition instanceof Val) { return $this->val($definition); } @@ -138,7 +125,24 @@ private function definition( return $this->run($definition, $id); } - throw new ContainerException(); // TODO: message + if (is_array($definition)) { + $items = []; + foreach ($definition as $key => $item) { + $items[] = new ArrayItem( + value: $this->definition($item), + key: is_int($key) ? new Int_(value: $key) : new String_(value: $key), + ); + } + + return new Array_(items: $items); + } + + throw new ContainerException( + sprintf( + "Expected definition, got '%s'.", + get_debug_type($definition), + ) + ); } private function val(Val $definition): Expr @@ -171,6 +175,15 @@ private function env(Env $definition): Expr private function ref(Ref $definition): Expr { + if (!isset($this->hashes[$definition->id])) { + // autowiring + $this->hashes[$definition->id] = md5($definition->id); + $this->method($definition->id, $this->hashes[$definition->id], new Obj($definition->id)); + } elseif (!isset($this->body[$definition->id])) { + // DFS + $this->method($definition->id, $this->hashes[$definition->id], $this->definitions->get($definition->id)); + } + return new Coalesce( left: new ArrayDimFetch( var: new PropertyFetch( @@ -181,7 +194,7 @@ private function ref(Ref $definition): Expr ), right: new StaticCall( class: new Name(name: 'self'), - name: "_{$this->map[$definition->id]}", + name: "_{$this->hashes[$definition->id]}", args: [ new Arg( value: new Variable(name: 'container'), @@ -494,20 +507,15 @@ private function autowire(ReflectionParameter $rp): Expr throw new ContainerException("Cannot autowire parameter {$reference} based on built-in type '{$rt->getName()}'."); } - $id = $rt->getName(); - if (isset($this->map[$id])) { - return $this->definition(new Ref($id)); - } - - $this->map[$id] = md5($id); - $this->postponed[] = $id; - - return $this->definition(new Ref($id)); + return $this->definition(new Ref($rt->getName())); } - private function method(string $id, string $hash, Expr $definition): ClassMethod + private function method(string $id, string $hash, mixed $definition): void { - return new ClassMethod( + $this->visitor->enter($id); + $expr = $this->definition($definition, $id); + + $this->body[$id] = new ClassMethod( name: "_{$hash}", subNodes: [ 'flags' => 12, @@ -527,11 +535,13 @@ private function method(string $id, string $hash, Expr $definition): ClassMethod ), dim: new String_(value: $id), ), - expr: $definition, + expr: $expr, ), ), ], ], ); + + $this->visitor->exit($id); } } diff --git a/src/Container.php b/src/Container.php index 0ea23d0..f1c6d8d 100644 --- a/src/Container.php +++ b/src/Container.php @@ -11,7 +11,6 @@ use Norvica\Container\Definition\Ref; use Norvica\Container\Definition\Run; use Norvica\Container\Definition\Val; -use Norvica\Container\Exception\CircularDependencyException; use Norvica\Container\Exception\ContainerException; use Norvica\Container\Exception\NotFoundException; use Psr\Container\ContainerExceptionInterface; @@ -33,16 +32,14 @@ final class Container implements ContainerInterface */ private array $resolved = []; - /** - * @var string[] - */ - private array $resolving = []; + private Visitor $visitor; public function __construct( private readonly Definitions $definitions, private readonly ContainerInterface|null $compiled = null, - private bool $autowiring = true, + private readonly bool $autowiring = true, ) { + $this->visitor = new Visitor(); } /** @@ -65,16 +62,7 @@ public function get(string $id): mixed return $this->compiled->get($id); } - if ($this->inProgress($id)) { - throw new CircularDependencyException( - sprintf( - "Circular dependency detected when resolving the following chain: '%s' → '{$id}'.", - implode("' → '", $this->resolving), - ) - ); - } - - $this->resolvingStarted($id); + $this->visitor->enter($id); // if ID is a class name, try to construct it, even if it's not registered explicitly if (!$this->definitions->has($id) && $this->autowiring) { @@ -84,14 +72,14 @@ public function get(string $id): mixed $resolved = $this->resolve(new Obj($id)); $this->resolved[$id] = $resolved; - $this->resolvingFinished($id); + $this->visitor->exit($id); return $resolved; } $resolved = $this->resolve($this->definitions->get($id)); $this->resolved[$id] = $resolved; - $this->resolvingFinished($id); + $this->visitor->exit($id); return $resolved; } @@ -248,24 +236,4 @@ private function guess(ReflectionParameter $rp): mixed return $this->get($rt->getName()); } - - private function inProgress(string $id): bool - { - return in_array($id, $this->resolving, true); - } - - private function resolvingStarted(string $id): void - { - $this->resolving[] = $id; - } - - private function resolvingFinished(string $id): void - { - $index = array_search($id, $this->resolving, true); - if ($index === false) { - throw new ContainerException("Tried to finish resolving for '{$id}' that hasn't been started."); - } - - unset($this->resolving[$index]); - } } diff --git a/src/Visitor.php b/src/Visitor.php new file mode 100644 index 0000000..4bac2f1 --- /dev/null +++ b/src/Visitor.php @@ -0,0 +1,48 @@ +visiting($id)) { + throw new CircularDependencyException( + sprintf( + "Circular dependency detected when resolving the following chain: '%s' → '{$id}'.", + implode("' → '", $this->visiting), + ) + ); + } + + $this->visiting[] = $id; + } + + public function exit(string $id): void + { + $index = array_search($id, $this->visiting, true); + if ($index === false) { + throw new ContainerException("Tried to exit node '{$id}' that hasn't been entered."); + } + + unset($this->visiting[$index]); + } + + private function visiting(string $id): bool + { + return in_array($id, $this->visiting, true); + } +} diff --git a/tests/Integration/CircularDependencyTest.php b/tests/Integration/CircularDependencyTest.php index e3d2bb2..817371a 100644 --- a/tests/Integration/CircularDependencyTest.php +++ b/tests/Integration/CircularDependencyTest.php @@ -37,11 +37,20 @@ public static function configuration(): Generator } #[DataProvider('configuration')] - public function test(array $configuration, string $id): void + public function testCold(array $configuration, string $id): void { $this->expectException(CircularDependencyException::class); $container = $this->container($configuration); $container->get($id); } + + #[DataProvider('configuration')] + public function testCompiled(array $configuration, string $id): void + { + $this->expectException(CircularDependencyException::class); + $container = $this->compiled($configuration); + + $container->get($id); + } }