[Debug] made Debug find FQCN automatically based on well-known autoloaders

This commit is contained in:
Fabien Potencier 2013-07-24 05:56:44 +02:00
parent 208ca5f8aa
commit 53ab284745
5 changed files with 111 additions and 12 deletions

View File

@ -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.
*/

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
2.4.0
-----
* improved error messages for not found classes and functions
2.3.0
-----

View File

@ -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;
}
}
}

View File

@ -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.',
),
);
}

View File

@ -0,0 +1,5 @@
<?php
class Symfony_Component_Debug_Tests_Fixtures_PEARClass
{
}