Skip to content

Commit 2908043

Browse files
authored
Merge pull request #582 from kukulich/autoload
`AutoloadSourceLocator` should support `phar://` paths as well as `file://`
2 parents 29a3bac + f741917 commit 2908043

File tree

5 files changed

+130
-20
lines changed

5 files changed

+130
-20
lines changed

src/SourceLocator/Type/AutoloadSourceLocator.php

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
*/
4949
class AutoloadSourceLocator extends AbstractSourceLocator
5050
{
51+
private const DEFAULT_STREAM_WRAPPER_PROTOCOLS = [
52+
'file',
53+
'phar',
54+
];
55+
5156
/** @var AstLocator */
5257
private $astLocator;
5358

@@ -60,21 +65,27 @@ class AutoloadSourceLocator extends AbstractSourceLocator
6065
/** @var NodeVisitorAbstract */
6166
private $constantVisitor;
6267

68+
/** @var string[] */
69+
private $streamWrapperProtocols;
70+
6371
/**
6472
* Note: the constructor has been made a 0-argument constructor because `\stream_wrapper_register`
6573
* is a piece of trash, and doesn't accept instances, just class names.
74+
*
75+
* @param string[] $streamWrapperProtocols
6676
*/
67-
public function __construct(?AstLocator $astLocator = null, ?Parser $phpParser = null)
77+
public function __construct(?AstLocator $astLocator = null, ?Parser $phpParser = null, array $streamWrapperProtocols = self::DEFAULT_STREAM_WRAPPER_PROTOCOLS)
6878
{
6979
$betterReflection = new BetterReflection();
7080

7181
$validLocator = $astLocator ?? self::$currentAstLocator ?? $betterReflection->astLocator();
7282

7383
parent::__construct($validLocator);
7484

75-
$this->astLocator = $validLocator;
76-
$this->phpParser = $phpParser ?? $betterReflection->phpParser();
77-
$this->constantVisitor = $this->createConstantVisitor();
85+
$this->astLocator = $validLocator;
86+
$this->phpParser = $phpParser ?? $betterReflection->phpParser();
87+
$this->streamWrapperProtocols = $streamWrapperProtocols;
88+
$this->constantVisitor = $this->createConstantVisitor();
7889

7990
$this->nodeTraverser = new NodeTraverser();
8091
$this->nodeTraverser->addVisitor(new NameResolver());
@@ -163,18 +174,34 @@ private function locateClassByName(string $className) : ?string
163174

164175
self::$autoloadLocatedFile = null;
165176
self::$currentAstLocator = $this->astLocator; // passing the locator on to the implicitly instantiated `self`
166-
$previousErrorHandler = set_error_handler(static function (int $errno, string $errstr) : bool {
167-
return true;
168-
});
169-
stream_wrapper_unregister('file');
170-
stream_wrapper_register('file', self::class);
171-
class_exists($className);
172-
stream_wrapper_restore('file');
173-
set_error_handler($previousErrorHandler);
177+
178+
$this->silenceErrors();
179+
180+
foreach ($this->streamWrapperProtocols as $protocol) {
181+
stream_wrapper_unregister($protocol);
182+
stream_wrapper_register($protocol, self::class);
183+
}
184+
185+
try {
186+
class_exists($className);
187+
} finally {
188+
foreach ($this->streamWrapperProtocols as $protocol) {
189+
stream_wrapper_restore($protocol);
190+
}
191+
192+
restore_error_handler();
193+
}
174194

175195
return self::$autoloadLocatedFile;
176196
}
177197

198+
private function silenceErrors() : void
199+
{
200+
set_error_handler(static function () : bool {
201+
return true;
202+
});
203+
}
204+
178205
/**
179206
* We can only load functions if they already exist, because PHP does not
180207
* have function autoloading. Therefore if it exists, we simply use the
@@ -275,21 +302,22 @@ public function stream_open($path, $mode, $options, &$opened_path) : bool
275302
*/
276303
public function url_stat($path, $flags)
277304
{
278-
stream_wrapper_restore('file');
305+
foreach ($this->streamWrapperProtocols as $protocol) {
306+
stream_wrapper_restore($protocol);
307+
}
279308

280309
if ($flags & STREAM_URL_STAT_QUIET) {
281-
set_error_handler(static function () {
282-
// Use native error handler
283-
return false;
284-
});
285-
$result = @stat($path);
286310
restore_error_handler();
311+
$result = @stat($path);
312+
$this->silenceErrors();
287313
} else {
288314
$result = stat($path);
289315
}
290316

291-
stream_wrapper_unregister('file');
292-
stream_wrapper_register('file', self::class);
317+
foreach ($this->streamWrapperProtocols as $protocol) {
318+
stream_wrapper_unregister($protocol);
319+
stream_wrapper_register($protocol, self::class);
320+
}
293321

294322
return $result;
295323
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Roave\BetterReflectionTest\Fixture;
4+
5+
use Exception;
6+
7+
class BrokenAutoloaderException extends Exception
8+
{
9+
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Roave\BetterReflectionTest\Fixture;
4+
5+
class ClassNotInPhar
6+
{
7+
}

test/unit/Fixture/autoload.phar

86.9 KB
Binary file not shown.

test/unit/SourceLocator/Type/AutoloadSourceLocatorTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@
1919
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
2020
use Roave\BetterReflection\SourceLocator\Type\AutoloadSourceLocator;
2121
use Roave\BetterReflectionTest\BetterReflectionSingleton;
22+
use Roave\BetterReflectionTest\Fixture\AutoloadableClassInPhar;
2223
use Roave\BetterReflectionTest\Fixture\AutoloadableInterface;
2324
use Roave\BetterReflectionTest\Fixture\AutoloadableTrait;
25+
use Roave\BetterReflectionTest\Fixture\BrokenAutoloaderException;
2426
use Roave\BetterReflectionTest\Fixture\ClassForHinting;
27+
use Roave\BetterReflectionTest\Fixture\ClassNotInPhar;
2528
use Roave\BetterReflectionTest\Fixture\ExampleClass;
2629
use function class_exists;
2730
use function file_exists;
31+
use function file_get_contents;
2832
use function interface_exists;
33+
use function restore_error_handler;
34+
use function set_error_handler;
2935
use function spl_autoload_register;
3036
use function spl_autoload_unregister;
3137
use function trait_exists;
@@ -302,4 +308,63 @@ public function autoload(string $className) : bool
302308

303309
return true;
304310
}
311+
312+
/**
313+
* @runInSeparateProcess
314+
*/
315+
public function testWillLocateSourcesInPharPath() : void
316+
{
317+
require_once 'phar://' . __DIR__ . '/../../Fixture/autoload.phar/vendor/autoload.php';
318+
spl_autoload_register(static function (string $class) : void {
319+
if ($class !== ClassNotInPhar::class) {
320+
return;
321+
}
322+
323+
include_once __DIR__ . '/../../Fixture/ClassNotInPhar.php';
324+
});
325+
326+
$sourceLocator = new AutoloadSourceLocator($this->astLocator);
327+
$classReflector = new ClassReflector($sourceLocator);
328+
329+
$reflection = $classReflector->reflect(AutoloadableClassInPhar::class);
330+
331+
$this->assertSame(AutoloadableClassInPhar::class, $reflection->getName());
332+
}
333+
334+
public function testBrokenAutoloader() : void
335+
{
336+
$getErrorHandler = static function () : ?callable {
337+
$errorHandler = set_error_handler(static function () : bool {
338+
return true;
339+
});
340+
restore_error_handler();
341+
342+
return $errorHandler;
343+
};
344+
345+
$toBeThrown = new BrokenAutoloaderException();
346+
$brokenAutoloader = static function () use ($toBeThrown) : void {
347+
throw $toBeThrown;
348+
};
349+
$previousErrorHandler = $getErrorHandler();
350+
351+
spl_autoload_register($brokenAutoloader);
352+
353+
try {
354+
(new AutoloadSourceLocator($this->astLocator))
355+
->locateIdentifier(
356+
$this->getMockReflector(),
357+
new Identifier('Whatever', new IdentifierType(IdentifierType::IDENTIFIER_CLASS))
358+
);
359+
360+
self::fail('No exception was thrown');
361+
} catch (BrokenAutoloaderException $e) {
362+
self::assertSame($e, $toBeThrown);
363+
} finally {
364+
spl_autoload_unregister($brokenAutoloader);
365+
}
366+
367+
self::assertSame($previousErrorHandler, $getErrorHandler());
368+
self::assertNotFalse(file_get_contents(__FILE__));
369+
}
305370
}

0 commit comments

Comments
 (0)