diff --git a/src/CompiledInjector.php b/src/CompiledInjector.php index 9dfcd114..77f1e0ab 100644 --- a/src/CompiledInjector.php +++ b/src/CompiledInjector.php @@ -8,6 +8,7 @@ use Ray\Compiler\Exception\ScriptDirNotReadable; use Ray\Compiler\Exception\Unbound; use Ray\Di\Annotation\ScriptDir; +use Ray\Di\InjectorInterface; use Ray\Di\Name; use function file_exists; @@ -66,11 +67,13 @@ public function __construct(#[ScriptDir] /** @psalm-var ScriptDir $realPath */ $this->scriptDir = $realPath; + $this->cacheInjector(); $this->registerLoader(); } public function __wakeup() { + $this->cacheInjector(); $this->registerLoader(); } @@ -105,6 +108,11 @@ public function getInstance($interface, $name = Name::ANY) return $instance; } + private function cacheInjector(): void + { + $this->singletons[InjectorInterface::class . '-' . Name::ANY] = $this; + } + private function registerLoader(): void { $scriptDir = $this->scriptDir; diff --git a/src/InstanceScript.php b/src/InstanceScript.php index fd2f068b..8c98a84b 100644 --- a/src/InstanceScript.php +++ b/src/InstanceScript.php @@ -158,7 +158,7 @@ public function pushAspectBind(AopBind $aopBind): void public function getScript(string|null $postConstruct, bool $isSingleton): string { if (is_string($postConstruct)) { - $this->laterLines[] = sprintf('$instance->%s();', $postConstruct); + $this->pushPostConstruct($postConstruct, $isSingleton); } if ($this->implementsSetContext) { @@ -166,7 +166,7 @@ public function getScript(string|null $postConstruct, bool $isSingleton): string } $this->laterLines[] = self::COMMENT; - if ($isSingleton) { + if ($isSingleton && ! is_string($postConstruct)) { $this->laterLines[] = '$singletons[$dependencyIndex] = $instance;'; } @@ -178,4 +178,21 @@ public function getScript(string|null $postConstruct, bool $isSingleton): string return $script; } + + private function pushPostConstruct(string $postConstruct, bool $isSingleton): void + { + if (! $isSingleton) { + $this->laterLines[] = sprintf('$instance->%s();', $postConstruct); + + return; + } + + $this->laterLines[] = '$singletons[$dependencyIndex] = $instance;'; + $this->laterLines[] = 'try {'; + $this->laterLines[] = sprintf(' $instance->%s();', $postConstruct); + $this->laterLines[] = '} catch (\Throwable $e) {'; + $this->laterLines[] = ' unset($singletons[$dependencyIndex]);'; + $this->laterLines[] = ' throw $e;'; + $this->laterLines[] = '}'; + } } diff --git a/tests/Fake/FakeFailingPostConstructSingleton.php b/tests/Fake/FakeFailingPostConstructSingleton.php new file mode 100644 index 00000000..735b7276 --- /dev/null +++ b/tests/Fake/FakeFailingPostConstructSingleton.php @@ -0,0 +1,37 @@ +initialized = true; + } + + public static function reset(): void + { + self::$constructorCalls = 0; + self::$postConstructCalls = 0; + } +} diff --git a/tests/Fake/FakePostConstructDependent.php b/tests/Fake/FakePostConstructDependent.php new file mode 100644 index 00000000..d441bba7 --- /dev/null +++ b/tests/Fake/FakePostConstructDependent.php @@ -0,0 +1,12 @@ + 1) { + throw new RuntimeException('Recursive PostConstruct call'); + } + + $dependent = $this->injector->getInstance(FakePostConstructDependent::class); + assert($dependent instanceof FakePostConstructDependent); + $this->dependent = $dependent; + } + + public static function reset(): void + { + self::$postConstructCalls = 0; + } +} diff --git a/tests/SingletonPostConstructTest.php b/tests/SingletonPostConstructTest.php new file mode 100644 index 00000000..45d9dab0 --- /dev/null +++ b/tests/SingletonPostConstructTest.php @@ -0,0 +1,69 @@ +bind(FakePostConstructSingleton::class)->in(Scope::SINGLETON); + $this->bind(FakePostConstructDependent::class); + $this->bind(FakeFailingPostConstructSingleton::class)->in(Scope::SINGLETON); + } + }; + (new Compiler())->compile($module, $scriptDir); + $this->injector = new CompiledInjector($scriptDir); + } + + public function testSingletonIsAvailableDuringPostConstruct(): void + { + $singleton = $this->injector->getInstance(FakePostConstructSingleton::class); + $this->assertInstanceOf(FakePostConstructSingleton::class, $singleton); + $this->assertInstanceOf(FakePostConstructDependent::class, $singleton->dependent); + $this->assertSame($singleton, $singleton->dependent->singleton); + $this->assertSame(1, FakePostConstructSingleton::$postConstructCalls); + $this->assertSame($this->injector, $this->injector->getInstance(InjectorInterface::class)); + } + + public function testFailedPostConstructIsRemovedFromSingletonCache(): void + { + try { + $this->injector->getInstance(FakeFailingPostConstructSingleton::class); + $this->fail('The first PostConstruct call must fail.'); + } catch (RuntimeException $e) { + $this->assertSame('PostConstruct failed', $e->getMessage()); + } + + $singleton = $this->injector->getInstance(FakeFailingPostConstructSingleton::class); + $this->assertInstanceOf(FakeFailingPostConstructSingleton::class, $singleton); + $this->assertTrue($singleton->initialized); + $this->assertSame(2, FakeFailingPostConstructSingleton::$constructorCalls); + $this->assertSame(2, FakeFailingPostConstructSingleton::$postConstructCalls); + $this->assertSame($singleton, $this->injector->getInstance(FakeFailingPostConstructSingleton::class)); + } +}