diff --git a/src/Symfony/Component/ClassLoader/DebugClassLoader.php b/src/Symfony/Component/ClassLoader/DebugClassLoader.php index 842f4744c0..9a6069fe68 100644 --- a/src/Symfony/Component/ClassLoader/DebugClassLoader.php +++ b/src/Symfony/Component/ClassLoader/DebugClassLoader.php @@ -39,6 +39,16 @@ class DebugClassLoader $this->classFinder = $classFinder; } + /** + * Gets the wrapped class loader. + * + * @return object a class loader instance + */ + public function getClassLoader() + { + return $this->classFinder; + } + /** * Replaces all autoloaders implementing a findFile method by a DebugClassLoader wrapper. */ diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index 2ad5ce695c..a8321da551 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.4.0 +----- + + * improved error messages for not found classes and functions + 2.3.0 ----- diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 9a6f13c788..b315106561 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -16,6 +16,9 @@ use Symfony\Component\Debug\Exception\ClassNotFoundException; use Symfony\Component\Debug\Exception\ContextErrorException; use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\Exception\UndefinedFunctionException; +use Composer\Autoload\ClassLoader as ComposerClassLoader; +use Symfony\Component\ClassLoader as SymfonyClassLoader; +use Symfony\Component\ClassLoader\DebugClassLoader; /** * ErrorHandler. @@ -288,7 +291,7 @@ class ErrorHandler ); } - if ($classes = $this->getUseStatementSuggestions($className)) { + if ($classes = $this->getClassCandidates($className)) { $message .= sprintf(' Perhaps you need to add a use statement for one of the following class: %s.', implode(', ', $classes)); } @@ -296,15 +299,82 @@ class ErrorHandler } } - protected function getUseStatementSuggestions($class) + /** + * 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 + * autoloader (that should cover all common cases). + * + * @param string $class A class name (without its namespace) + * + * @return array An array of possible fully qualified class names + */ + private function getClassCandidates($class) { - $classNameToUseStatementSuggestions = array( - 'Request' => array('Symfony\Component\HttpFoundation\Request'), - 'Response' => array('Symfony\Component\HttpFoundation\Response'), - ); + if (!is_array($functions = spl_autoload_functions())) { + return array(); + } - if (isset($classNameToUseStatementSuggestions[$class])) { - return $classNameToUseStatementSuggestions[$class]; + // find Symfony and Composer autoloaders + $classes = array(); + foreach ($functions as $function) { + if (!is_array($function)) { + continue; + } + + // get class loaders wrapped by DebugClassLoader + if ($function[0] instanceof DebugClassLoader && method_exists($function[0], 'getClassLoader')) { + $function[0] = $function[0]->getClassLoader(); + } + + if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) { + foreach ($function[0]->getPrefixes() as $paths) { + foreach ($paths as $path) { + $classes = array_merge($classes, $this->findClassInPath($function[0], $path, $class)); + } + } + } + } + + return $classes; + } + + private function findClassInPath($loader, $path, $class) + { + if (!$path = realpath($path)) { + continue; + } + + $classes = array(); + $filename = $class.'.php'; + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if ($filename == $file->getFileName() && $class = $this->convertFileToClass($loader, $path, $file->getPathName())) { + $classes[] = $class; + } + } + + return $classes; + } + + private function convertFileToClass($loader, $path, $file) + { + // 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. + require_once $file; + + $file = str_replace(array($path.'/', '.php'), array('', ''), $file); + + // is it a namespaced class? + $class = str_replace('/', '\\', $file); + if (class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false))) { + return $class; + } + + // is it a PEAR-like class name instead? + $class = str_replace('/', '_', $file); + if (class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false))) { + return $class; } } } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index e2cada866c..19504c1b73 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -119,18 +119,27 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase 'type' => 1, 'line' => 12, 'file' => 'foo.php', - 'message' => 'Class "Request" not found', + 'message' => 'Class "UndefinedFunctionException" not found', ), - 'Attempted to load class "Request" from the global namespace in foo.php line 12. Did you forget a use statement for this class? Perhaps you need to add a use statement for one of the following class: Symfony\Component\HttpFoundation\Request.', + 'Attempted to load class "UndefinedFunctionException" from the global namespace in foo.php line 12. Did you forget a use statement for this class? Perhaps you need to add a use statement for one of the following class: Symfony\Component\Debug\Exception\UndefinedFunctionException.', ), array( array( 'type' => 1, 'line' => 12, 'file' => 'foo.php', - 'message' => 'Class "Foo\\Bar\\Request" not found', + 'message' => 'Class "PEARClass" not found', ), - 'Attempted to load class "Request" from namespace "Foo\Bar" in foo.php line 12. Do you need to "use" it from another namespace? Perhaps you need to add a use statement for one of the following class: Symfony\Component\HttpFoundation\Request.', + 'Attempted to load class "PEARClass" from the global namespace in foo.php line 12. Did you forget a use statement for this class? Perhaps you need to add a use statement for one of the following class: Symfony_Component_Debug_Tests_Fixtures_PEARClass.', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class "Foo\\Bar\\UndefinedFunctionException" not found', + ), + 'Attempted to load class "UndefinedFunctionException" from namespace "Foo\Bar" in foo.php line 12. Do you need to "use" it from another namespace? Perhaps you need to add a use statement for one of the following class: Symfony\Component\Debug\Exception\UndefinedFunctionException.', ), ); } diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php b/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php new file mode 100644 index 0000000000..39f228182e --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php @@ -0,0 +1,5 @@ +