Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ public function compile(AbstractModule $module, string $scriptDir): Scripts
$compileVisitor = new CompileVisitor($container);
$container->map(static function (DependencyInterface $dependency, string $key) use ($scripts, $compileVisitor): DependencyInterface {
assert($dependency instanceof AcceptInterface);
if ($key === InstanceScript::RAY_DI_SCRIPT_DIR) {
$scripts->add($key, 'return __DIR__;');

return $dependency;
}

$script = $dependency->accept($compileVisitor);
assert(is_string($script));
$scripts->add($key, $script);
Expand Down
9 changes: 9 additions & 0 deletions src/InstanceScript.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ final class InstanceScript
{
public const RAY_DI_INJECTOR_INTERFACE = 'Ray\Di\InjectorInterface-';
public const RAY_DI_INJECTION_POINT_INTERFACE = 'Ray\Di\InjectionPointInterface-';
public const RAY_DI_SCRIPT_DIR = '-Ray\Di\Annotation\ScriptDir';
public const COMMENT = '// prototype';

/** @var array<mixed> */
Expand All @@ -57,6 +58,14 @@ public function __construct(Container $container)
/** @param mixed $defaultValue */
public function addArg(string $index, bool $isDefaultAvailable, $defaultValue, ReflectionParameter $parameter): void
{
// The script dir is where the generated scripts live: resolve it at
// runtime, never bake the compile-time path.
if ($index === self::RAY_DI_SCRIPT_DIR) {
$this->args[] = '__DIR__';

return;
}

if (! isset($this->container[$index])) {
if ($isDefaultAvailable) {
$this->addInstanceArg($defaultValue);
Expand Down
19 changes: 19 additions & 0 deletions tests/Fake/FakeScriptDirAopConsumer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use Ray\Di\Annotation\ScriptDir;

class FakeScriptDirAopConsumer implements FakeScriptDirConsumerInterface
{
public function __construct(#[ScriptDir] private string $scriptDir)
{
}

public function getScriptDir(): string
{
return $this->scriptDir;
}
}
19 changes: 19 additions & 0 deletions tests/Fake/FakeScriptDirConstructorConsumer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use Ray\Di\Annotation\ScriptDir;

class FakeScriptDirConstructorConsumer implements FakeScriptDirConsumerInterface
{
public function __construct(#[ScriptDir] private string $scriptDir)
{
}

public function getScriptDir(): string
{
return $this->scriptDir;
}
}
10 changes: 10 additions & 0 deletions tests/Fake/FakeScriptDirConsumerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

interface FakeScriptDirConsumerInterface
{
public function getScriptDir(): string;
}
23 changes: 23 additions & 0 deletions tests/Fake/FakeScriptDirModule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use Ray\Di\AbstractModule;

class FakeScriptDirModule extends AbstractModule
{
protected function configure(): void
{
$this->bind(FakeScriptDirConsumerInterface::class)->annotatedWith('ctor')->to(FakeScriptDirConstructorConsumer::class);
$this->bind(FakeScriptDirConsumerInterface::class)->annotatedWith('setter')->to(FakeScriptDirSetterConsumer::class);
$this->bind(FakeScriptDirConsumerInterface::class)->annotatedWith('provider')->toProvider(FakeScriptDirProvider::class);
$this->bind(FakeScriptDirConsumerInterface::class)->annotatedWith('aop')->to(FakeScriptDirAopConsumer::class);
$this->bindInterceptor(
$this->matcher->subclassesOf(FakeScriptDirAopConsumer::class),
$this->matcher->any(),
[FakeInterceptor::class],
);
}
}
21 changes: 21 additions & 0 deletions tests/Fake/FakeScriptDirProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use Ray\Di\Annotation\ScriptDir;
use Ray\Di\ProviderInterface;

/** @implements ProviderInterface<FakeScriptDirConsumerInterface> */
class FakeScriptDirProvider implements ProviderInterface
{
public function __construct(#[ScriptDir] private string $scriptDir)
{
}

public function get(): FakeScriptDirConsumerInterface
{
return new FakeScriptDirConstructorConsumer($this->scriptDir);
}
}
24 changes: 24 additions & 0 deletions tests/Fake/FakeScriptDirSetterConsumer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use Ray\Di\Annotation\ScriptDir;
use Ray\Di\Di\Inject;

class FakeScriptDirSetterConsumer implements FakeScriptDirConsumerInterface
{
private string $scriptDir = '';

#[Inject]
public function setScriptDir(#[ScriptDir] string $scriptDir): void
{
$this->scriptDir = $scriptDir;
}

public function getScriptDir(): string
{
return $this->scriptDir;
}
}
152 changes: 152 additions & 0 deletions tests/ScriptDirRelocationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

declare(strict_types=1);

namespace Ray\Compiler;

use FilesystemIterator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Ray\Di\Annotation\ScriptDir;
use Ray\Di\InjectorInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

use function array_filter;
use function array_values;
use function assert;
use function file_get_contents;
use function implode;
use function mkdir;
use function preg_replace;
use function realpath;
use function rename;
use function str_contains;
use function str_ends_with;

/**
* Compiled scripts must not bake the compile-time absolute path: everything
* needed to locate them is derivable from __DIR__ at require-time, so a
* compiled script directory can be moved after compilation.
*/
class ScriptDirRelocationTest extends TestCase
{
public static function setUpBeforeClass(): void
{
deleteFiles(__DIR__ . '/tmp');
}

private string $buildDir;
private string $runtimeDir;

protected function setUp(): void
{
$case = $this->name() . '-' . preg_replace('/[^A-Za-z0-9_-]+/', '_', (string) $this->dataName());
$this->buildDir = __DIR__ . '/tmp/script-dir-relocation-' . $case . '-build';
$this->runtimeDir = __DIR__ . '/tmp/script-dir-relocation-' . $case . '-runtime';
@mkdir($this->buildDir, 0777, true);
(new Compiler())->compile(new FakeScriptDirModule(), $this->buildDir);
}

public function testGeneratedScriptDirScriptContainsNoBakedPath(): void
{
$script = file_get_contents($this->buildDir . '/-Ray_Di_Annotation_ScriptDir.php');
$this->assertSame("<?php\nreturn __DIR__;", $script);
}

/**
* CompilerModule also toInstance()-binds the unrelated Compile::class
* annotation (a bool) right next to ScriptDir. The __DIR__ special case
* in Compiler::compile()'s map() callback must match on the ScriptDir
* key only, not swallow this neighboring instance binding.
*/
public function testCompileAnnotationInstanceBindingStillCompilesNormally(): void
{
$script = file_get_contents($this->buildDir . '/-Ray_Compiler_Annotation_Compile.php');
$this->assertSame("<?php\nreturn true;", $script);
}

public function testNoGeneratedScriptContainsTheCompileTimePath(): void
{
$phpFiles = $this->findGeneratedPhpFiles($this->buildDir);
$this->assertNotEmpty($phpFiles, 'expected at least one generated *.php script to be scanned');

// Collect every offender instead of asserting inside the loop, so a
// single run reports all leaking scripts, not just the first one.
$leaking = array_values(array_filter(
$phpFiles,
fn (string $path): bool => str_contains((string) file_get_contents($path), $this->buildDir),
));

$this->assertSame([], $leaking, 'these generated scripts must not contain the compile-time path: ' . implode(', ', $leaking));
}

/** @return list<string> */
private function findGeneratedPhpFiles(string $dir): array
{
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS));
$files = [];
foreach ($iterator as $file) {
$path = (string) $file;
if (str_ends_with($path, '.php')) {
$files[] = $path;
}
}

return $files;
}

/** @return array<string, array{0: string}> */
public static function provideBindingNames(): array
{
return [
'constructor injection' => ['ctor'],
'setter injection' => ['setter'],
'provider' => ['provider'],
'AOP-woven class' => ['aop'],
];
}

#[DataProvider('provideBindingNames')]
public function testRelocatedBindingResolvesToTheRuntimeDir(string $bindingName): void
{
[$injector, $runtimeDir] = $this->relocateAndCreateInjector();

$instance = $injector->getInstance(FakeScriptDirConsumerInterface::class, $bindingName);
$this->assertInstanceOf(FakeScriptDirConsumerInterface::class, $instance);
$this->assertSame($runtimeDir, $instance->getScriptDir());
}

public function testRelocatedScriptDirInstanceBindingResolvesToTheRuntimeDir(): void
{
[$injector, $runtimeDir] = $this->relocateAndCreateInjector();

$scriptDir = $injector->getInstance('', ScriptDir::class);
$this->assertSame($runtimeDir, $scriptDir);
}

public function testRelocatedInjectorInterfaceBindingIsUsable(): void
{
[$injector, $runtimeDir] = $this->relocateAndCreateInjector();

$innerInjector = $injector->getInstance(InjectorInterface::class);
$this->assertInstanceOf(CompiledInjector::class, $innerInjector);

$instance = $innerInjector->getInstance(FakeScriptDirConsumerInterface::class, 'ctor');
$this->assertSame($runtimeDir, $instance->getScriptDir());
}

/** @return array{0: CompiledInjector, 1: string} */
private function relocateAndCreateInjector(): array
{
$this->assertTrue(rename($this->buildDir, $this->runtimeDir), 'failed to relocate the compiled script dir');

$runtimeDir = $this->runtimeDir;
assert($runtimeDir !== '');

$realRuntimeDir = realpath($runtimeDir);
$this->assertIsString($realRuntimeDir);

return [new CompiledInjector($runtimeDir), $realRuntimeDir];
}
}
Loading