diff --git a/docs/performance.md b/docs/performance.md index bb507ebb..829045d8 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -56,7 +56,7 @@ return require $file; // happy path: no stat(), just cached opcode A missing compiled script is a build invariant violation (corrupt or incomplete build); PHP 8 makes a failed `require` a catchable `Error` that surfaces naturally. `CompiledInjector::getInstance()` keeps its `file_exists()` pre-check (it reports unbound interfaces as `Unbound`); its redundant -`realpath($this->scriptDir)` — already canonicalised in the constructor — was removed. +`realpath($this->scriptDir)` — already validated in the constructor — was removed. ## Benchmarking correctly diff --git a/src/CompiledInjector.php b/src/CompiledInjector.php index eace33ab..595a47bc 100644 --- a/src/CompiledInjector.php +++ b/src/CompiledInjector.php @@ -66,8 +66,13 @@ final class CompiledInjector implements ScriptInjectorInterface public function __construct(#[ScriptDir] string $scriptDir,) { + // realpath() cannot resolve stream URIs such as phar://; keep those as given $realPath = realpath($scriptDir); - if ($realPath === false || ! is_dir($realPath) || ! is_readable($realPath)) { + if ($realPath === false) { + $realPath = $scriptDir; + } + + if (! is_dir($realPath) || ! is_readable($realPath)) { $message = sprintf('Script directory "%s" is not readable. See https://ray-di.github.io/Ray.Compiler/error/ScriptDirNotReadable', $scriptDir); throw new ScriptDirNotReadable($message); @@ -112,7 +117,7 @@ public function getInstance($interface, $name = Name::ANY) /** @psalm-suppress UnsupportedPropertyReferenceUsage */ $singletons = &$this->singletons; - $scriptDir = $this->scriptDir; // already realpath()d in the constructor + $scriptDir = $this->scriptDir; // validated in the constructor // $scriptDir, $singletons, and $dependencyIndex can be used in the included file /** @var mixed $instance */ diff --git a/tests/CompiledInjectorTest.php b/tests/CompiledInjectorTest.php index 896803af..1d8dd46b 100644 --- a/tests/CompiledInjectorTest.php +++ b/tests/CompiledInjectorTest.php @@ -10,11 +10,17 @@ use Ray\Compiler\Exception\ScriptDirNotReadable; use Ray\Di\Exception\Unbound; +use function escapeshellarg; +use function exec; +use function implode; use function mkdir; use function serialize; use function spl_object_hash; +use function sprintf; use function unserialize; +use const PHP_BINARY; + /** @psalm-import-type ScriptDir from CompiledInjector */ class CompiledInjectorTest extends TestCase { @@ -111,4 +117,29 @@ public function testThrowsScriptDirNotReadableException(): void $this->expectExceptionMessage($scriptDir); new CompiledInjector($scriptDir); } + + /** + * realpath() returns false for phar:// paths, but compiled scripts are + * relocatable (see ScriptDirRelocationTest) and phars can carry them. + */ + public function testScriptDirInsidePhar(): void + { + $scriptDir = __DIR__ . '/tmp/' . __FUNCTION__; + @mkdir($scriptDir); + (new Compiler())->compile(new FakeModule(), $scriptDir); + $pharFile = $scriptDir . '.phar'; // outside $scriptDir so the build never globs its own output + // The test process runs with phar.readonly=1; build in a child that does not + exec(sprintf( + '%s -d phar.readonly=0 %s %s %s 2>&1', + escapeshellarg(PHP_BINARY), + escapeshellarg(__DIR__ . '/script/build_phar.php'), + escapeshellarg($scriptDir), + escapeshellarg($pharFile), + ), $output, $exitCode); + $this->assertSame(0, $exitCode, implode("\n", $output)); + + $injector = new CompiledInjector('phar://' . $pharFile . '/di'); + $instance = $injector->getInstance(FakeCarInterface::class); + $this->assertInstanceOf(FakeCarInterface::class, $instance); + } } diff --git a/tests/script/build_phar.php b/tests/script/build_phar.php new file mode 100644 index 00000000..7f5ca5bc --- /dev/null +++ b/tests/script/build_phar.php @@ -0,0 +1,20 @@ +startBuffering(); +foreach (array_filter((array) glob($argv[1] . '/*')) as $file) { + $phar->addFile($file, 'di/' . basename($file)); +} + +$phar->stopBuffering();