Developer friendly Class Not Found and Undefined Function errors.

This commit is contained in:
Beau Simensen 2013-05-28 12:12:41 -05:00 committed by Fabien Potencier
parent 1d86ea10ff
commit 667194574d
5 changed files with 394 additions and 4 deletions

View File

@ -11,9 +11,11 @@
namespace Symfony\Component\Debug;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\ContextErrorException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\Exception\ClassNotFoundException;
use Symfony\Component\Debug\Exception\ContextErrorException;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\UndefinedFunctionException;
/**
* ErrorHandler.
@ -41,6 +43,11 @@ class ErrorHandler
E_PARSE => 'Parse',
);
private $classNameToUseStatementSuggestions = array(
'Request' => 'Symfony\Component\HttpFoundation\Request',
'Response' => 'Symfony\Component\HttpFoundation\Response',
);
private $level;
private $reservedMemory;
@ -152,15 +159,158 @@ class ErrorHandler
return;
}
$this->handleFatalError($error);
}
public function handleFatalError($error)
{
// get current exception handler
$exceptionHandler = set_exception_handler(function() {});
restore_exception_handler();
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) {
$level = isset($this->levels[$type]) ? $this->levels[$type] : $type;
$level = isset($this->levels[$error['type']]) ? $this->levels[$error['type']] : $error['type'];
$message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']);
$exception = new FatalErrorException($message, 0, $type, $error['file'], $error['line']);
$exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line']);
if ($this->handleUndefinedFunctionError($exceptionHandler[0], $error, $exception)) {
return;
}
if ($this->handleClassNotFoundError($exceptionHandler[0], $error, $exception)) {
return;
}
$exceptionHandler[0]->handle($exception);
}
}
private function handleUndefinedFunctionError($exceptionHandler, $error, $exception)
{
$messageLen = strlen($error['message']);
$notFoundSuffix = "()";
$notFoundSuffixLen = strlen($notFoundSuffix);
if ($notFoundSuffixLen > $messageLen) {
return false;
}
if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) {
return false;
}
$prefix = "Call to undefined function ";
$prefixLen = strlen($prefix);
if (0 !== strpos($error['message'], $prefix)) {
return false;
}
$fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen);
if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) {
$functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1);
$namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex);
$message = sprintf(
"Attempted to call function '%s' from namespace '%s' in %s line %d.",
$functionName,
$namespacePrefix,
$error['file'],
$error['line']
);
} else {
$functionName = $fullyQualifiedFunctionName;
$message = sprintf(
"Attempted to call function '%s' from the global namespace in %s line %d.",
$functionName,
$error['file'],
$error['line']
);
}
$candidates = array();
foreach (get_defined_functions() as $type => $definedFunctionNames) {
foreach ($definedFunctionNames as $definedFunctionName) {
if (false !== $namespaceSeparatorIndex = strrpos($definedFunctionName, '\\')) {
$definedFunctionNameBasename = substr($definedFunctionName, $namespaceSeparatorIndex + 1);
} else {
$definedFunctionNameBasename = $definedFunctionName;
}
if ($definedFunctionNameBasename === $functionName) {
$candidates[] = '\\'.$definedFunctionName;
}
}
}
if ($candidates) {
$message .= " Did you mean to call: " . implode(", ", array_map(function ($val) {
return "'".$val."'";
}, $candidates)). "?";
}
$exceptionHandler->handle(new UndefinedFunctionException(
$message,
$exception
));
return true;
}
private function handleClassNotFoundError($exceptionHandler, $error, $exception)
{
$messageLen = strlen($error['message']);
$notFoundSuffix = "' not found";
$notFoundSuffixLen = strlen($notFoundSuffix);
if ($notFoundSuffixLen > $messageLen) {
return false;
}
if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) {
return false;
}
foreach (array("class", "interface", "trait") as $typeName) {
$prefix = ucfirst($typeName)." '";
$prefixLen = strlen($prefix);
if (0 !== strpos($error['message'], $prefix)) {
continue;
}
$fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen);
if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) {
$className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1);
$namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex);
$message = sprintf(
"Attempted to load %s '%s' from namespace '%s' in %s line %d. Do you need to 'use' it from another namespace?",
$typeName,
$className,
$namespacePrefix,
$error['file'],
$error['line']
);
} else {
$className = $fullyQualifiedClassName;
$message = sprintf(
"Attempted to load %s '%s' from the global namespace in %s line %d. Did you forget a use statement for this %s?",
$typeName,
$className,
$error['file'],
$error['line'],
$typeName
);
}
if (isset($this->classNameToUseStatementSuggestions[$className])) {
$message .= sprintf(
" Perhaps you need to add 'use %s' at the top of this file?",
$this->classNameToUseStatementSuggestions[$className]
);
}
$exceptionHandler->handle(new ClassNotFoundException(
$message,
$exception
));
return true;
}
}
}

View File

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Exception;
/**
* Class (or Trait or Interface) Not Found Exception.
*
* @author Konstanton Myakshin <koc-dp@yandex.ru>
*/
class ClassNotFoundException extends \ErrorException
{
public function __construct($message, \ErrorException $previous)
{
parent::__construct(
$message,
$previous->getCode(),
$previous->getSeverity(),
$previous->getFile(),
$previous->getLine(),
$previous->getPrevious()
);
}
}

View File

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Exception;
/**
* Undefined Function Exception.
*
* @author Konstanton Myakshin <koc-dp@yandex.ru>
*/
class UndefinedFunctionException extends \ErrorException
{
public function __construct($message, \ErrorException $previous)
{
parent::__construct(
$message,
$previous->getCode(),
$previous->getSeverity(),
$previous->getFile(),
$previous->getLine(),
$previous->getPrevious()
);
}
}

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\Debug\Tests;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\Exception\ClassNotFoundException;
use Symfony\Component\Debug\ExceptionHandler;
/**
* ErrorHandlerTest
@ -90,4 +92,154 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase
restore_error_handler();
}
public function provideClassNotFoundData()
{
return array(
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Class 'WhizBangFactory' not found",
),
"Attempted to load class 'WhizBangFactory' from the global namespace in foo.php line 12. Did you forget a use statement for this class?",
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Class 'Foo\\Bar\\WhizBangFactory' not found",
),
"Attempted to load class 'WhizBangFactory' from namespace 'Foo\\Bar' in foo.php line 12. Do you need to 'use' it from another namespace?",
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Class 'Request' 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 'use Symfony\\Component\\HttpFoundation\\Request' at the top of this file?",
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Class 'Foo\\Bar\\Request' 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 'use Symfony\\Component\\HttpFoundation\\Request' at the top of this file?",
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Class 'Response' not found",
),
"Attempted to load class 'Response' from the global namespace in foo.php line 12. Did you forget a use statement for this class? Perhaps you need to add 'use Symfony\\Component\\HttpFoundation\\Response' at the top of this file?",
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Class 'Foo\\Bar\\Response' not found",
),
"Attempted to load class 'Response' from namespace 'Foo\\Bar' in foo.php line 12. Do you need to 'use' it from another namespace? Perhaps you need to add 'use Symfony\\Component\\HttpFoundation\\Response' at the top of this file?",
),
);
}
/**
* @dataProvider provideClassNotFoundData
*/
public function testClassNotFound($error, $translatedMessage)
{
$handler = ErrorHandler::register(3);
$exceptionHandler = new MockExceptionHandler;
set_exception_handler(array($exceptionHandler, 'handle'));
$handler->handleFatalError($error);
$this->assertNotNull($exceptionHandler->e);
$this->assertSame($translatedMessage, $exceptionHandler->e->getMessage());
$this->assertSame($error['type'], $exceptionHandler->e->getSeverity());
$this->assertSame($error['file'], $exceptionHandler->e->getFile());
$this->assertSame($error['line'], $exceptionHandler->e->getLine());
restore_exception_handler();
restore_error_handler();
}
public function provideUndefinedFunctionData()
{
return array(
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Call to undefined function test_namespaced_function()",
),
"Attempted to call function 'test_namespaced_function' from the global namespace in foo.php line 12. Did you mean to call: '\\symfony\\component\\debug\\tests\\test_namespaced_function'?",
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Call to undefined function Foo\\Bar\\Baz\\test_namespaced_function()",
),
"Attempted to call function 'test_namespaced_function' from namespace 'Foo\\Bar\\Baz' in foo.php line 12. Did you mean to call: '\\symfony\\component\\debug\\tests\\test_namespaced_function'?",
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Call to undefined function foo()",
),
"Attempted to call function 'foo' from the global namespace in foo.php line 12.",
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => "Call to undefined function Foo\\Bar\\Baz\\foo()",
),
"Attempted to call function 'foo' from namespace 'Foo\Bar\Baz' in foo.php line 12.",
),
);
}
/**
* @dataProvider provideUndefinedFunctionData
*/
public function testUndefinedFunction($error, $translatedMessage)
{
$handler = ErrorHandler::register(3);
$exceptionHandler = new MockExceptionHandler;
set_exception_handler(array($exceptionHandler, 'handle'));
$handler->handleFatalError($error);
$this->assertNotNull($exceptionHandler->e);
$this->assertSame($translatedMessage, $exceptionHandler->e->getMessage());
$this->assertSame($error['type'], $exceptionHandler->e->getSeverity());
$this->assertSame($error['file'], $exceptionHandler->e->getFile());
$this->assertSame($error['line'], $exceptionHandler->e->getLine());
restore_exception_handler();
restore_error_handler();
}
}
function test_namespaced_function()
{
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests;
use Symfony\Component\Debug\ExceptionHandler;
class MockExceptionHandler extends Exceptionhandler
{
public $e;
public function handle(\Exception $e)
{
$this->e = $e;
}
}