From 98ed078029072fbaac7fe4a0f5e6603abbaab28b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 22 Apr 2015 20:47:54 +0200 Subject: [PATCH] [Debug] Fix ClassNotFoundFatalErrorHandler candidates lookups --- .../ClassNotFoundFatalErrorHandler.php | 27 ++++-- .../ClassNotFoundFatalErrorHandlerTest.php | 92 ++++++++++++------- 2 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php index c75f17edff..abfe90d792 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -77,7 +77,7 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface /** * 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). * * @param string $class A class name (without its namespace) @@ -101,7 +101,7 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface if ($function[0] instanceof DebugClassLoader) { $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)) { $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); @@ -132,13 +139,13 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface */ 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(); } $classes = array(); $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)) { $classes[] = $class; } @@ -160,13 +167,21 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface // namespaced class $namespacedClass = str_replace(array($path.DIRECTORY_SEPARATOR, '.php', '/'), array('', '', '\\'), $file), // 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 str_replace('\\', '_', $namespacedClass), // 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 // is not found, the new autoloader call will require the file again leading to a // "cannot redeclare class" error. diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php index 06b22a83c8..c4b35fd2f7 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -15,18 +15,52 @@ use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; use Symfony\Component\ClassLoader\UniversalClassLoader as SymfonyUniversalClassLoader; use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +use Symfony\Component\Debug\DebugClassLoader; +use Composer\Autoload\ClassLoader as ComposerClassLoader; 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 */ - 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(); $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->assertSame($translatedMessage, $exception->getMessage()); $this->assertSame($error['type'], $exception->getSeverity()); @@ -35,35 +69,37 @@ class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase } /** - * @dataProvider provideLegacyClassNotFoundData * @group legacy */ - public function testLegacyHandleClassNotFound($error, $translatedMessage, $autoloader) + public function testLegacyHandleClassNotFound() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); - // 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); + $prefixes = array('Symfony\Component\Debug\Exception\\' => realpath(__DIR__.'/../../Exception')); + $symfonyUniversalClassLoader = new SymfonyUniversalClassLoader(); + $symfonyUniversalClassLoader->registerPrefixes($prefixes); - $handler = new ClassNotFoundFatalErrorHandler(); - - $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); - - spl_autoload_unregister($autoloader); - array_map('spl_autoload_register', $autoloaders); - - $this->assertInstanceof('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception); - $this->assertSame($translatedMessage, $exception->getMessage()); - $this->assertSame($error['type'], $exception->getSeverity()); - $this->assertSame($error['file'], $exception->getFile()); - $this->assertSame($error['line'], $exception->getLine()); + $this->testHandleClassNotFound( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + '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\"?", + array($symfonyUniversalClassLoader, 'loadClass') + ); } public function provideClassNotFoundData() { + $prefixes = array('Symfony\Component\Debug\Exception\\' => realpath(__DIR__.'/../../Exception')); + + $symfonyAutoloader = new SymfonyClassLoader(); + $symfonyAutoloader->addPrefixes($prefixes); + + $debugClassLoader = new DebugClassLoader($symfonyAutoloader); + return 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\"?", ), - ); - } - - 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( 'type' => 1, @@ -142,7 +164,7 @@ class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase '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\"?", - array($symfonyUniversalClassLoader, 'loadClass'), + array($debugClassLoader, 'loadClass'), ), array( array(