[Debug] Fix ClassNotFoundFatalErrorHandler candidates lookups

This commit is contained in:
Nicolas Grekas 2015-04-22 20:47:54 +02:00
parent 386f7332c9
commit 98ed078029
2 changed files with 78 additions and 41 deletions

View File

@ -77,7 +77,7 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface
/** /**
* Tries to guess the full namespace for a given class name. * Tries to guess the full namespace for a given class name.
* *
* By default, it looks for PSR-0 classes registered via a Symfony or a Composer * By default, it looks for PSR-0 and PSR-4 classes registered via a Symfony or a Composer
* autoloader (that should cover all common cases). * autoloader (that should cover all common cases).
* *
* @param string $class A class name (without its namespace) * @param string $class A class name (without its namespace)
@ -101,7 +101,7 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface
if ($function[0] instanceof DebugClassLoader) { if ($function[0] instanceof DebugClassLoader) {
$function = $function[0]->getClassLoader(); $function = $function[0]->getClassLoader();
// Since 2.5, returning an object from DebugClassLoader::getClassLoader() is @deprecated // @deprecated since version 2.5. Returning an object from DebugClassLoader::getClassLoader() is deprecated.
if (is_object($function)) { if (is_object($function)) {
$function = array($function); $function = array($function);
} }
@ -118,6 +118,13 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface
} }
} }
} }
if ($function[0] instanceof ComposerClassLoader) {
foreach ($function[0]->getPrefixesPsr4() as $prefix => $paths) {
foreach ($paths as $path) {
$classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix));
}
}
}
} }
return array_unique($classes); return array_unique($classes);
@ -132,13 +139,13 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface
*/ */
private function findClassInPath($path, $class, $prefix) private function findClassInPath($path, $class, $prefix)
{ {
if (!$path = realpath($path)) { if (!$path = realpath($path.'/'.strtr($prefix, '\\_', '//')) ?: realpath($path.'/'.dirname(strtr($prefix, '\\_', '//'))) ?: realpath($path)) {
return array(); return array();
} }
$classes = array(); $classes = array();
$filename = $class.'.php'; $filename = $class.'.php';
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) { if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) {
$classes[] = $class; $classes[] = $class;
} }
@ -160,13 +167,21 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface
// namespaced class // namespaced class
$namespacedClass = str_replace(array($path.DIRECTORY_SEPARATOR, '.php', '/'), array('', '', '\\'), $file), $namespacedClass = str_replace(array($path.DIRECTORY_SEPARATOR, '.php', '/'), array('', '', '\\'), $file),
// namespaced class (with target dir) // namespaced class (with target dir)
$namespacedClassTargetDir = $prefix.str_replace(array($path.DIRECTORY_SEPARATOR, '.php', '/'), array('', '', '\\'), $file), $prefix.$namespacedClass,
// namespaced class (with target dir and separator)
$prefix.'\\'.$namespacedClass,
// PEAR class // PEAR class
str_replace('\\', '_', $namespacedClass), str_replace('\\', '_', $namespacedClass),
// PEAR class (with target dir) // PEAR class (with target dir)
str_replace('\\', '_', $namespacedClassTargetDir), str_replace('\\', '_', $prefix.$namespacedClass),
// PEAR class (with target dir and separator)
str_replace('\\', '_', $prefix.'\\'.$namespacedClass),
); );
if ($prefix) {
$candidates = array_filter($candidates, function ($candidate) use ($prefix) {return 0 === strpos($candidate, $prefix);});
}
// We cannot use the autoloader here as most of them use require; but if the class // We cannot use the autoloader here as most of them use require; but if the class
// is not found, the new autoloader call will require the file again leading to a // is not found, the new autoloader call will require the file again leading to a
// "cannot redeclare class" error. // "cannot redeclare class" error.

View File

@ -15,18 +15,52 @@ use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader;
use Symfony\Component\ClassLoader\UniversalClassLoader as SymfonyUniversalClassLoader; use Symfony\Component\ClassLoader\UniversalClassLoader as SymfonyUniversalClassLoader;
use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
use Symfony\Component\Debug\DebugClassLoader;
use Composer\Autoload\ClassLoader as ComposerClassLoader;
class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase
{ {
public static function setUpBeforeClass()
{
foreach (spl_autoload_functions() as $function) {
if (!is_array($function)) {
continue;
}
// get class loaders wrapped by DebugClassLoader
if ($function[0] instanceof DebugClassLoader) {
$function = $function[0]->getClassLoader();
}
if ($function[0] instanceof ComposerClassLoader) {
$function[0]->add('Symfony_Component_Debug_Tests_Fixtures', dirname(dirname(dirname(dirname(dirname(__DIR__))))));
break;
}
}
}
/** /**
* @dataProvider provideClassNotFoundData * @dataProvider provideClassNotFoundData
*/ */
public function testHandleClassNotFound($error, $translatedMessage) public function testHandleClassNotFound($error, $translatedMessage, $autoloader = null)
{ {
if ($autoloader) {
// Unregister all autoloaders to ensure the custom provided
// autoloader is the only one to be used during the test run.
$autoloaders = spl_autoload_functions();
array_map('spl_autoload_unregister', $autoloaders);
spl_autoload_register($autoloader);
}
$handler = new ClassNotFoundFatalErrorHandler(); $handler = new ClassNotFoundFatalErrorHandler();
$exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line']));
if ($autoloader) {
spl_autoload_unregister($autoloader);
array_map('spl_autoload_register', $autoloaders);
}
$this->assertInstanceof('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception); $this->assertInstanceof('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception);
$this->assertSame($translatedMessage, $exception->getMessage()); $this->assertSame($translatedMessage, $exception->getMessage());
$this->assertSame($error['type'], $exception->getSeverity()); $this->assertSame($error['type'], $exception->getSeverity());
@ -35,35 +69,37 @@ class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase
} }
/** /**
* @dataProvider provideLegacyClassNotFoundData
* @group legacy * @group legacy
*/ */
public function testLegacyHandleClassNotFound($error, $translatedMessage, $autoloader) public function testLegacyHandleClassNotFound()
{ {
$this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED);
// Unregister all autoloaders to ensure the custom provided $prefixes = array('Symfony\Component\Debug\Exception\\' => realpath(__DIR__.'/../../Exception'));
// autoloader is the only one to be used during the test run. $symfonyUniversalClassLoader = new SymfonyUniversalClassLoader();
$autoloaders = spl_autoload_functions(); $symfonyUniversalClassLoader->registerPrefixes($prefixes);
array_map('spl_autoload_unregister', $autoloaders);
spl_autoload_register($autoloader);
$handler = new ClassNotFoundFatalErrorHandler(); $this->testHandleClassNotFound(
array(
$exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); 'type' => 1,
'line' => 12,
spl_autoload_unregister($autoloader); 'file' => 'foo.php',
array_map('spl_autoload_register', $autoloaders); 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found',
),
$this->assertInstanceof('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception); "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?",
$this->assertSame($translatedMessage, $exception->getMessage()); array($symfonyUniversalClassLoader, 'loadClass')
$this->assertSame($error['type'], $exception->getSeverity()); );
$this->assertSame($error['file'], $exception->getFile());
$this->assertSame($error['line'], $exception->getLine());
} }
public function provideClassNotFoundData() public function provideClassNotFoundData()
{ {
$prefixes = array('Symfony\Component\Debug\Exception\\' => realpath(__DIR__.'/../../Exception'));
$symfonyAutoloader = new SymfonyClassLoader();
$symfonyAutoloader->addPrefixes($prefixes);
$debugClassLoader = new DebugClassLoader($symfonyAutoloader);
return array( return array(
array( array(
array( array(
@ -110,20 +146,6 @@ class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase
), ),
"Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?",
), ),
);
}
public function provideLegacyClassNotFoundData()
{
$prefixes = array('Symfony\Component\Debug\Exception\\' => realpath(__DIR__.'/../../Exception'));
$symfonyAutoloader = new SymfonyClassLoader();
$symfonyAutoloader->addPrefixes($prefixes);
$symfonyUniversalClassLoader = new SymfonyUniversalClassLoader();
$symfonyUniversalClassLoader->registerPrefixes($prefixes);
return array(
array( array(
array( array(
'type' => 1, 'type' => 1,
@ -142,7 +164,7 @@ class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase
'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found',
), ),
"Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?",
array($symfonyUniversalClassLoader, 'loadClass'), array($debugClassLoader, 'loadClass'),
), ),
array( array(
array( array(