diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index dfb7dad003..28c0d7a83e 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -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; + } + } } diff --git a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php new file mode 100644 index 0000000000..c7c0ced2c6 --- /dev/null +++ b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php @@ -0,0 +1,32 @@ + + * + * 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 + */ +class ClassNotFoundException extends \ErrorException +{ + public function __construct($message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + $previous->getPrevious() + ); + } +} diff --git a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php new file mode 100644 index 0000000000..7a272df406 --- /dev/null +++ b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php @@ -0,0 +1,32 @@ + + * + * 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 + */ +class UndefinedFunctionException extends \ErrorException +{ + public function __construct($message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + $previous->getPrevious() + ); + } +} diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index db06f6107e..fd39c545e5 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -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() +{ } diff --git a/src/Symfony/Component/Debug/Tests/MockExceptionHandler.php b/src/Symfony/Component/Debug/Tests/MockExceptionHandler.php new file mode 100644 index 0000000000..a85d2d15e7 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/MockExceptionHandler.php @@ -0,0 +1,24 @@ + + * + * 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; + } +}