Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/CompiledInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down
21 changes: 19 additions & 2 deletions src/InstanceScript.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,15 @@ 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) {
$this->laterLines[] = sprintf('$instance->setContext(%s);', var_export($this->context, true));
}

$this->laterLines[] = self::COMMENT;
if ($isSingleton) {
if ($isSingleton && ! is_string($postConstruct)) {
$this->laterLines[] = '$singletons[$dependencyIndex] = $instance;';
}

Expand All @@ -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[] = '}';
}
}
37 changes: 37 additions & 0 deletions tests/Fake/FakeFailingPostConstructSingleton.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use Ray\Di\Di\PostConstruct;
use RuntimeException;

final class FakeFailingPostConstructSingleton
{
public static int $constructorCalls = 0;
public static int $postConstructCalls = 0;
public bool $initialized = false;

public function __construct()
{
self::$constructorCalls++;
}

#[PostConstruct]
public function initialize(): void
{
self::$postConstructCalls++;
if (self::$postConstructCalls === 1) {
throw new RuntimeException('PostConstruct failed');
}

$this->initialized = true;
}

public static function reset(): void
{
self::$constructorCalls = 0;
self::$postConstructCalls = 0;
}
}
12 changes: 12 additions & 0 deletions tests/Fake/FakePostConstructDependent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

final class FakePostConstructDependent
{
public function __construct(public readonly FakePostConstructSingleton $singleton)
{
}
}
39 changes: 39 additions & 0 deletions tests/Fake/FakePostConstructSingleton.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use Ray\Di\Di\PostConstruct;
use Ray\Di\InjectorInterface;
use RuntimeException;

use function assert;

final class FakePostConstructSingleton
{
public static int $postConstructCalls = 0;
public FakePostConstructDependent|null $dependent = null;

public function __construct(private readonly InjectorInterface $injector)
{
}

#[PostConstruct]
public function initialize(): void
{
self::$postConstructCalls++;
if (self::$postConstructCalls > 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;
}
}
69 changes: 69 additions & 0 deletions tests/SingletonPostConstructTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use PHPUnit\Framework\TestCase;
use Ray\Di\AbstractModule;
use Ray\Di\InjectorInterface;
use Ray\Di\Scope;
use RuntimeException;

use function is_dir;
use function mkdir;

final class SingletonPostConstructTest extends TestCase
{
private CompiledInjector $injector;

protected function setUp(): void
{
$scriptDir = __DIR__ . '/tmp/singleton-post-construct';
if (! is_dir($scriptDir)) {
mkdir($scriptDir, 0777, true);
}

deleteFiles($scriptDir);
FakeFailingPostConstructSingleton::reset();
FakePostConstructSingleton::reset();

$module = new class extends AbstractModule {
protected function configure(): void
{
$this->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));
}
}
Loading