From 667194574dd3253b58bad05f97b87aa84de9b87f Mon Sep 17 00:00:00 2001 From: Beau Simensen Date: Tue, 28 May 2013 12:12:41 -0500 Subject: [PATCH 1/8] Developer friendly Class Not Found and Undefined Function errors. --- src/Symfony/Component/Debug/ErrorHandler.php | 158 +++++++++++++++++- .../Exception/ClassNotFoundException.php | 32 ++++ .../Exception/UndefinedFunctionException.php | 32 ++++ .../Debug/Tests/ErrorHandlerTest.php | 152 +++++++++++++++++ .../Debug/Tests/MockExceptionHandler.php | 24 +++ 5 files changed, 394 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Debug/Exception/ClassNotFoundException.php create mode 100644 src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php create mode 100644 src/Symfony/Component/Debug/Tests/MockExceptionHandler.php diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index dfb7dad003f4f..28c0d7a83e49f 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 @@ public function handleFatal() 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 0000000000000..c7c0ced2c6870 --- /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 0000000000000..7a272df406313 --- /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 db06f6107e6ec..fd39c545e591f 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 @@ public function testHandle() 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 0000000000000..a85d2d15e7131 --- /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; + } +} From a0b1585d3c92e40c676768ba0d78999451f04244 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 23 Jul 2013 20:05:03 +0200 Subject: [PATCH 2/8] [Debug] fixed CS --- src/Symfony/Component/Debug/ErrorHandler.php | 67 ++++++++----------- .../Debug/Tests/ErrorHandlerTest.php | 58 ++++++---------- 2 files changed, 49 insertions(+), 76 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 28c0d7a83e49f..ed4dc67c32c59 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -162,7 +162,7 @@ public function handleFatal() $this->handleFatalError($error); } - public function handleFatalError($error) + private function handleFatalError($error) { // get current exception handler $exceptionHandler = set_exception_handler(function() {}); @@ -173,35 +173,35 @@ public function handleFatalError($error) $message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']); $exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line']); - if ($this->handleUndefinedFunctionError($exceptionHandler[0], $error, $exception)) { - return; + if ($ex = $this->handleUndefinedFunctionError($error, $exception)) { + return $exceptionHandler[0]->handle($ex); } - if ($this->handleClassNotFoundError($exceptionHandler[0], $error, $exception)) { - return; + if ($ex = $this->handleClassNotFoundError($error, $exception)) { + return $exceptionHandler[0]->handle($ex); } $exceptionHandler[0]->handle($exception); } } - private function handleUndefinedFunctionError($exceptionHandler, $error, $exception) + private function handleUndefinedFunctionError($error, $exception) { $messageLen = strlen($error['message']); - $notFoundSuffix = "()"; + $notFoundSuffix = '()'; $notFoundSuffixLen = strlen($notFoundSuffix); if ($notFoundSuffixLen > $messageLen) { - return false; + return; } if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { - return false; + return; } - $prefix = "Call to undefined function "; + $prefix = 'Call to undefined function '; $prefixLen = strlen($prefix); if (0 !== strpos($error['message'], $prefix)) { - return false; + return; } $fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); @@ -209,7 +209,7 @@ private function handleUndefinedFunctionError($exceptionHandler, $error, $except $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); $message = sprintf( - "Attempted to call function '%s' from namespace '%s' in %s line %d.", + 'Attempted to call function "%s" from namespace "%s" in %s line %d.', $functionName, $namespacePrefix, $error['file'], @@ -218,7 +218,7 @@ private function handleUndefinedFunctionError($exceptionHandler, $error, $except } else { $functionName = $fullyQualifiedFunctionName; $message = sprintf( - "Attempted to call function '%s' from the global namespace in %s line %d.", + 'Attempted to call function "%s" from the global namespace in %s line %d.', $functionName, $error['file'], $error['line'] @@ -241,34 +241,29 @@ private function handleUndefinedFunctionError($exceptionHandler, $error, $except } if ($candidates) { - $message .= " Did you mean to call: " . implode(", ", array_map(function ($val) { - return "'".$val."'"; - }, $candidates)). "?"; + $message .= ' Did you mean to call: '.implode(', ', array_map(function ($val) { + return '"'.$val.'"'; + }, $candidates)).'?'; } - $exceptionHandler->handle(new UndefinedFunctionException( - $message, - $exception - )); - - return true; + return new UndefinedFunctionException($message, $exception); } - private function handleClassNotFoundError($exceptionHandler, $error, $exception) + private function handleClassNotFoundError($error, $exception) { $messageLen = strlen($error['message']); - $notFoundSuffix = "' not found"; + $notFoundSuffix = '" not found'; $notFoundSuffixLen = strlen($notFoundSuffix); if ($notFoundSuffixLen > $messageLen) { - return false; + return; } if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { - return false; + return; } - foreach (array("class", "interface", "trait") as $typeName) { - $prefix = ucfirst($typeName)." '"; + foreach (array('class', 'interface', 'trait') as $typeName) { + $prefix = ucfirst($typeName).' "'; $prefixLen = strlen($prefix); if (0 !== strpos($error['message'], $prefix)) { continue; @@ -279,7 +274,7 @@ private function handleClassNotFoundError($exceptionHandler, $error, $exception) $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?", + 'Attempted to load %s "%s" from namespace "%s" in %s line %d. Do you need to "use" it from another namespace?', $typeName, $className, $namespacePrefix, @@ -289,7 +284,7 @@ private function handleClassNotFoundError($exceptionHandler, $error, $exception) } 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?", + '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'], @@ -299,18 +294,10 @@ private function handleClassNotFoundError($exceptionHandler, $error, $exception) } if (isset($this->classNameToUseStatementSuggestions[$className])) { - $message .= sprintf( - " Perhaps you need to add 'use %s' at the top of this file?", - $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; + return new ClassNotFoundException($message, $exception); } } } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index fd39c545e591f..9868d7e7ef388 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -101,54 +101,36 @@ public function provideClassNotFoundData() 'type' => 1, 'line' => 12, 'file' => 'foo.php', - 'message' => "Class 'WhizBangFactory' not found", + '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?", + '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", + '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?", + '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", + '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?", + '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", + '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?", + '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?', ), ); } @@ -159,11 +141,13 @@ public function provideClassNotFoundData() public function testClassNotFound($error, $translatedMessage) { $handler = ErrorHandler::register(3); + $m = new \ReflectionMethod($handler, 'handleFatalError'); + $m->setAccessible(true); $exceptionHandler = new MockExceptionHandler; set_exception_handler(array($exceptionHandler, 'handle')); - $handler->handleFatalError($error); + $m->invoke($handler, $error); $this->assertNotNull($exceptionHandler->e); $this->assertSame($translatedMessage, $exceptionHandler->e->getMessage()); @@ -183,36 +167,36 @@ public function provideUndefinedFunctionData() 'type' => 1, 'line' => 12, 'file' => 'foo.php', - 'message' => "Call to undefined function test_namespaced_function()", + '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'?", + '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()", + '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'?", + '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()", + 'message' => 'Call to undefined function foo()', ), - "Attempted to call function 'foo' from the global namespace in foo.php line 12.", + '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()", + 'message' => 'Call to undefined function Foo\\Bar\\Baz\\foo()', ), - "Attempted to call function 'foo' from namespace 'Foo\Bar\Baz' in foo.php line 12.", + 'Attempted to call function "foo" from namespace "Foo\Bar\Baz" in foo.php line 12.', ), ); } @@ -223,11 +207,13 @@ public function provideUndefinedFunctionData() public function testUndefinedFunction($error, $translatedMessage) { $handler = ErrorHandler::register(3); + $m = new \ReflectionMethod($handler, 'handleFatalError'); + $m->setAccessible(true); $exceptionHandler = new MockExceptionHandler; set_exception_handler(array($exceptionHandler, 'handle')); - $handler->handleFatalError($error); + $m->invoke($handler, $error); $this->assertNotNull($exceptionHandler->e); $this->assertSame($translatedMessage, $exceptionHandler->e->getMessage()); From 208ca5f8aa60451986acf8c6e9ecca4b9ccf574d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 24 Jul 2013 05:31:30 +0200 Subject: [PATCH 3/8] [Debug] made guessing of possible class names more flexible --- src/Symfony/Component/Debug/ErrorHandler.php | 21 ++++++++++++------- .../Debug/Tests/ErrorHandlerTest.php | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index ed4dc67c32c59..9a6f13c788ef1 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -43,11 +43,6 @@ class ErrorHandler E_PARSE => 'Parse', ); - private $classNameToUseStatementSuggestions = array( - 'Request' => 'Symfony\Component\HttpFoundation\Request', - 'Response' => 'Symfony\Component\HttpFoundation\Response', - ); - private $level; private $reservedMemory; @@ -293,11 +288,23 @@ private function handleClassNotFoundError($error, $exception) ); } - if (isset($this->classNameToUseStatementSuggestions[$className])) { - $message .= sprintf(' Perhaps you need to add "use %s" at the top of this file?', $this->classNameToUseStatementSuggestions[$className]); + if ($classes = $this->getUseStatementSuggestions($className)) { + $message .= sprintf(' Perhaps you need to add a use statement for one of the following class: %s.', implode(', ', $classes)); } return new ClassNotFoundException($message, $exception); } } + + protected function getUseStatementSuggestions($class) + { + $classNameToUseStatementSuggestions = array( + 'Request' => array('Symfony\Component\HttpFoundation\Request'), + 'Response' => array('Symfony\Component\HttpFoundation\Response'), + ); + + if (isset($classNameToUseStatementSuggestions[$class])) { + return $classNameToUseStatementSuggestions[$class]; + } + } } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 9868d7e7ef388..e2cada866c862 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -121,7 +121,7 @@ public function provideClassNotFoundData() '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?', + '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.', ), array( array( @@ -130,7 +130,7 @@ public function provideClassNotFoundData() '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?', + '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.', ), ); } From 53ab28474587ae687d2de34a93468b184209415a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 24 Jul 2013 05:56:44 +0200 Subject: [PATCH 4/8] [Debug] made Debug find FQCN automatically based on well-known autoloaders --- .../ClassLoader/DebugClassLoader.php | 10 +++ src/Symfony/Component/Debug/CHANGELOG.md | 5 ++ src/Symfony/Component/Debug/ErrorHandler.php | 86 +++++++++++++++++-- .../Debug/Tests/ErrorHandlerTest.php | 17 +++- .../Debug/Tests/Fixtures/PEARClass.php | 5 ++ 5 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php diff --git a/src/Symfony/Component/ClassLoader/DebugClassLoader.php b/src/Symfony/Component/ClassLoader/DebugClassLoader.php index 842f4744c0f66..9a6069fe68bc7 100644 --- a/src/Symfony/Component/ClassLoader/DebugClassLoader.php +++ b/src/Symfony/Component/ClassLoader/DebugClassLoader.php @@ -39,6 +39,16 @@ public function __construct($classFinder) $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 2ad5ce695c365..a8321da551b26 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 9a6f13c788ef1..b315106561913 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -16,6 +16,9 @@ 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 @@ private function handleClassNotFoundError($error, $exception) ); } - 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 @@ private function handleClassNotFoundError($error, $exception) } } - 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) + { + if (!is_array($functions = spl_autoload_functions())) { + return array(); + } + + // 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) { - $classNameToUseStatementSuggestions = array( - 'Request' => array('Symfony\Component\HttpFoundation\Request'), - 'Response' => array('Symfony\Component\HttpFoundation\Response'), - ); + 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; + } - if (isset($classNameToUseStatementSuggestions[$class])) { - return $classNameToUseStatementSuggestions[$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 e2cada866c862..19504c1b732e1 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -119,18 +119,27 @@ public function provideClassNotFoundData() '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 0000000000000..39f228182e0fa --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php @@ -0,0 +1,5 @@ + Date: Wed, 24 Jul 2013 06:51:29 +0200 Subject: [PATCH 5/8] [Debug] moved special fatal error handlers to their own classes --- src/Symfony/Component/Debug/ErrorHandler.php | 220 ++---------------- .../Exception/ClassNotFoundException.php | 2 +- .../Exception/UndefinedFunctionException.php | 2 +- .../ClassNotFoundFatalErrorHandler.php | 160 +++++++++++++ .../FatalErrorHandlerInterface.php | 32 +++ .../UndefinedFunctionFatalErrorHandler.php | 90 +++++++ 6 files changed, 297 insertions(+), 209 deletions(-) create mode 100644 src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php create mode 100644 src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php create mode 100644 src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index b315106561913..ec1518e279b12 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -12,13 +12,10 @@ namespace Symfony\Component\Debug; 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; -use Composer\Autoload\ClassLoader as ComposerClassLoader; -use Symfony\Component\ClassLoader as SymfonyClassLoader; -use Symfony\Component\ClassLoader\DebugClassLoader; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; +use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; /** * ErrorHandler. @@ -160,6 +157,14 @@ public function handleFatal() $this->handleFatalError($error); } + public function getFatalErrorHandlers() + { + return array( + new UndefinedFunctionFatalErrorHandler(), + new ClassNotFoundFatalErrorHandler(), + ); + } + private function handleFatalError($error) { // get current exception handler @@ -171,210 +176,11 @@ private function handleFatalError($error) $message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']); $exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line']); - if ($ex = $this->handleUndefinedFunctionError($error, $exception)) { - return $exceptionHandler[0]->handle($ex); - } - - if ($ex = $this->handleClassNotFoundError($error, $exception)) { - return $exceptionHandler[0]->handle($ex); - } - - $exceptionHandler[0]->handle($exception); - } - } - - private function handleUndefinedFunctionError($error, $exception) - { - $messageLen = strlen($error['message']); - $notFoundSuffix = '()'; - $notFoundSuffixLen = strlen($notFoundSuffix); - if ($notFoundSuffixLen > $messageLen) { - return; - } - - if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { - return; - } - - $prefix = 'Call to undefined function '; - $prefixLen = strlen($prefix); - if (0 !== strpos($error['message'], $prefix)) { - return; - } - - $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)).'?'; - } - - return new UndefinedFunctionException($message, $exception); - } - - private function handleClassNotFoundError($error, $exception) - { - $messageLen = strlen($error['message']); - $notFoundSuffix = '" not found'; - $notFoundSuffixLen = strlen($notFoundSuffix); - if ($notFoundSuffixLen > $messageLen) { - return; - } - - if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { - return; - } - - 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 ($classes = $this->getClassCandidates($className)) { - $message .= sprintf(' Perhaps you need to add a use statement for one of the following class: %s.', implode(', ', $classes)); - } - - return new ClassNotFoundException($message, $exception); - } - } - - /** - * 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) - { - if (!is_array($functions = spl_autoload_functions())) { - return array(); - } - - // 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)); - } + foreach ($this->getFatalErrorHandlers() as $handler) { + if ($ex = $handler->handleError($error, $exception)) { + return $exceptionHandler[0]->handle($ex); } } } - - 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/Exception/ClassNotFoundException.php b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php index c7c0ced2c6870..94169b45a2075 100644 --- a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php +++ b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php @@ -16,7 +16,7 @@ * * @author Konstanton Myakshin */ -class ClassNotFoundException extends \ErrorException +class ClassNotFoundException extends FatalErrorException { public function __construct($message, \ErrorException $previous) { diff --git a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php index 7a272df406313..572c8b30a1a7b 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php @@ -16,7 +16,7 @@ * * @author Konstanton Myakshin */ -class UndefinedFunctionException extends \ErrorException +class UndefinedFunctionException extends FatalErrorException { public function __construct($message, \ErrorException $previous) { diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php new file mode 100644 index 0000000000000..ee5ee7f32c82c --- /dev/null +++ b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\ClassNotFoundException; +use Symfony\Component\Debug\Exception\FatalErrorException; +use Composer\Autoload\ClassLoader as ComposerClassLoader; +use Symfony\Component\ClassLoader as SymfonyClassLoader; +use Symfony\Component\ClassLoader\DebugClassLoader; + +/** + * ErrorHandler for classes that do not exist. + * + * @author Fabien Potencier + */ +class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + $messageLen = strlen($error['message']); + $notFoundSuffix = '" not found'; + $notFoundSuffixLen = strlen($notFoundSuffix); + if ($notFoundSuffixLen > $messageLen) { + return; + } + + if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + return; + } + + 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 ($classes = $this->getClassCandidates($className)) { + $message .= sprintf(' Perhaps you need to add a use statement for one of the following class: %s.', implode(', ', $classes)); + } + + return new ClassNotFoundException($message, $exception); + } + } + + /** + * 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) + { + if (!is_array($functions = spl_autoload_functions())) { + return array(); + } + + // 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/FatalErrorHandler/FatalErrorHandlerInterface.php b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php new file mode 100644 index 0000000000000..6b87eb30a126e --- /dev/null +++ b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.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\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\FatalErrorException; + +/** + * Attempts to convert fatal errors to exceptions. + * + * @author Fabien Potencier + */ +interface FatalErrorHandlerInterface +{ + /** + * Attempts to convert an error into an exception. + * + * @param array $error An array as returned by error_get_last() + * @param FatalErrorException $exception A FatalErrorException instance + * + * @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise + */ + public function handleError(array $error, FatalErrorException $exception); +} diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php new file mode 100644 index 0000000000000..4b6fc905c9e25 --- /dev/null +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\UndefinedFunctionException; +use Symfony\Component\Debug\Exception\FatalErrorException; + +/** + * ErrorHandler for undefined functions. + * + * @author Fabien Potencier + */ +class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + $messageLen = strlen($error['message']); + $notFoundSuffix = '()'; + $notFoundSuffixLen = strlen($notFoundSuffix); + if ($notFoundSuffixLen > $messageLen) { + return; + } + + if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + return; + } + + $prefix = 'Call to undefined function '; + $prefixLen = strlen($prefix); + if (0 !== strpos($error['message'], $prefix)) { + return; + } + + $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)).'?'; + } + + return new UndefinedFunctionException($message, $exception); + } +} From 968764b5315dcabfe4c53847216316e232556032 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 24 Jul 2013 06:59:37 +0200 Subject: [PATCH 6/8] [Debug] refactored unit tests --- src/Symfony/Component/Debug/ErrorHandler.php | 30 ++-- .../Debug/Tests/ErrorHandlerTest.php | 131 ++---------------- .../ClassNotFoundFatalErrorHandlerTest.php | 85 ++++++++++++ ...UndefinedFunctionFatalErrorHandlerTest.php | 80 +++++++++++ 4 files changed, 196 insertions(+), 130 deletions(-) create mode 100644 src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php create mode 100644 src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index ec1518e279b12..d043aa5ec1a22 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -154,7 +154,13 @@ public function handleFatal() return; } - $this->handleFatalError($error); + // get current exception handler + $exceptionHandler = set_exception_handler(function() {}); + restore_exception_handler(); + + if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) { + $this->handleFatalError($exceptionHandler[0], $error); + } } public function getFatalErrorHandlers() @@ -165,22 +171,18 @@ public function getFatalErrorHandlers() ); } - private function handleFatalError($error) + private function handleFatalError(ExceptionHandler $exceptionHandler, array $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[$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, $error['type'], $error['file'], $error['line']); + $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, $error['type'], $error['file'], $error['line']); - foreach ($this->getFatalErrorHandlers() as $handler) { - if ($ex = $handler->handleError($error, $exception)) { - return $exceptionHandler[0]->handle($ex); - } + foreach ($this->getFatalErrorHandlers() as $handler) { + if ($ex = $handler->handleError($error, $exception)) { + return $exceptionHandler->handle($ex); } } + + $exceptionHandler->handle($exception); } } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 19504c1b732e1..81eef501c0672 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Debug\Tests; use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\Debug\Exception\ClassNotFoundException; -use Symfony\Component\Debug\ExceptionHandler; /** * ErrorHandlerTest @@ -22,7 +20,6 @@ */ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase { - public function testConstruct() { $handler = ErrorHandler::register(3); @@ -93,84 +90,29 @@ public function testHandle() 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 "UndefinedFunctionException" not found', - ), - '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 "PEARClass" not found', - ), - '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.', - ), - ); - } - /** - * @dataProvider provideClassNotFoundData + * @dataProvider provideFatalErrorHandlersData */ - public function testClassNotFound($error, $translatedMessage) + public function testFatalErrorHandlers($error, $class, $translatedMessage) { - $handler = ErrorHandler::register(3); + $handler = new ErrorHandler(); + $exceptionHandler = new MockExceptionHandler(); + $m = new \ReflectionMethod($handler, 'handleFatalError'); $m->setAccessible(true); + $m->invoke($handler, $exceptionHandler, $error); - $exceptionHandler = new MockExceptionHandler; - set_exception_handler(array($exceptionHandler, 'handle')); - - $m->invoke($handler, $error); - - $this->assertNotNull($exceptionHandler->e); + $this->assertInstanceof($class, $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() + public function provideFatalErrorHandlersData() { return array( + // undefined function array( array( 'type' => 1, @@ -178,63 +120,20 @@ public function provideUndefinedFunctionData() '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"?', + 'Symfony\Component\Debug\Exception\UndefinedFunctionException', + '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\\fatalerrorhandler\\test_namespaced_function"?', ), + // class not found 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()', + 'message' => 'Class "WhizBangFactory" not found', ), - 'Attempted to call function "foo" from namespace "Foo\Bar\Baz" in foo.php line 12.', + 'Symfony\Component\Debug\Exception\ClassNotFoundException', + 'Attempted to load class "WhizBangFactory" from the global namespace in foo.php line 12. Did you forget a use statement for this class?', ), ); } - - /** - * @dataProvider provideUndefinedFunctionData - */ - public function testUndefinedFunction($error, $translatedMessage) - { - $handler = ErrorHandler::register(3); - $m = new \ReflectionMethod($handler, 'handleFatalError'); - $m->setAccessible(true); - - $exceptionHandler = new MockExceptionHandler; - set_exception_handler(array($exceptionHandler, 'handle')); - - $m->invoke($handler, $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/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php new file mode 100644 index 0000000000000..d9663964c2d52 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Tests\FatalErrorHandler; + +use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; + +class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider provideClassNotFoundData + */ + public function testClassNotFound($error, $translatedMessage) + { + $handler = new ClassNotFoundFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $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()); + } + + 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 "UndefinedFunctionException" not found', + ), + '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 "PEARClass" not found', + ), + '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/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php new file mode 100644 index 0000000000000..8ba08cb2aa97d --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Tests\FatalErrorHandler; + +use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; + +class UndefinedFunctionFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider provideUndefinedFunctionData + */ + public function testUndefinedFunction($error, $translatedMessage) + { + $handler = new UndefinedFunctionFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $this->assertInstanceof('Symfony\Component\Debug\Exception\UndefinedFunctionException', $exception); + $this->assertSame($translatedMessage, $exception->getMessage()); + $this->assertSame($error['type'], $exception->getSeverity()); + $this->assertSame($error['file'], $exception->getFile()); + $this->assertSame($error['line'], $exception->getLine()); + } + + 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\\fatalerrorhandler\\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\\fatalerrorhandler\\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.', + ), + ); + } +} + +function test_namespaced_function() +{ +} From 80e19e269af2a5a39b5e74a4e1a7b188d732da64 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 24 Jul 2013 07:23:37 +0200 Subject: [PATCH 7/8] [Debug] added some missing phpdocs --- src/Symfony/Component/Debug/ErrorHandler.php | 28 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index d043aa5ec1a22..8afa49ee64eef 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -16,6 +16,7 @@ use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface; /** * ErrorHandler. @@ -57,7 +58,7 @@ class ErrorHandler /** * Registers the error handler. * - * @param integer $level The level at which the conversion to Exception is done (null to use the error_reporting() value and 0 to disable) + * @param integer $level The level at which the conversion to Exception is done (null to use the error_reporting() value and 0 to disable) * @param Boolean $displayErrors Display errors (for dev environment) or just log they (production usage) * * @return The registered error handler @@ -76,16 +77,32 @@ public static function register($level = null, $displayErrors = true) return $handler; } + /** + * Sets the level at which the conversion to Exception is done. + * + * @param integer|null $level The level (null to use the error_reporting() value and 0 to disable) + */ public function setLevel($level) { $this->level = null === $level ? error_reporting() : $level; } + /** + * Sets the display_errors flag value. + * + * @param integer $displayErrors The display_errors flag value + */ public function setDisplayErrors($displayErrors) { $this->displayErrors = $displayErrors; } + /** + * Sets a logger for the given channel. + * + * @param LoggerInterface $logger A logger interface + * @param string $channel The channel associated with the logger (deprecation or emergency) + */ public static function setLogger(LoggerInterface $logger, $channel = 'deprecation') { self::$loggers[$channel] = $logger; @@ -163,7 +180,14 @@ public function handleFatal() } } - public function getFatalErrorHandlers() + /** + * Gets the fatal error handlers. + * + * Override this method if you want to define more fatal error handlers. + * + * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface + */ + protected function getFatalErrorHandlers() { return array( new UndefinedFunctionFatalErrorHandler(), From bde67f099695617d373c46afd47383ac285a7535 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Jul 2013 22:44:44 +0200 Subject: [PATCH 8/8] fixed an error message --- .../FatalErrorHandler/ClassNotFoundFatalErrorHandler.php | 2 +- .../ClassNotFoundFatalErrorHandlerTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php index ee5ee7f32c82c..0bf60d894a260 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -72,7 +72,7 @@ public function handleError(array $error, FatalErrorException $exception) } if ($classes = $this->getClassCandidates($className)) { - $message .= sprintf(' Perhaps you need to add a use statement for one of the following class: %s.', implode(', ', $classes)); + $message .= sprintf(' Perhaps you need to add a use statement for one of the following: %s.', implode(', ', $classes)); } return new ClassNotFoundException($message, $exception); diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php index d9663964c2d52..7112c8c5f3e53 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -60,7 +60,7 @@ public function provideClassNotFoundData() 'file' => 'foo.php', 'message' => 'Class "UndefinedFunctionException" not found', ), - '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.', + '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: Symfony\Component\Debug\Exception\UndefinedFunctionException.', ), array( array( @@ -69,7 +69,7 @@ public function provideClassNotFoundData() 'file' => 'foo.php', 'message' => 'Class "PEARClass" not found', ), - '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.', + '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: Symfony_Component_Debug_Tests_Fixtures_PEARClass.', ), array( array( @@ -78,7 +78,7 @@ public function provideClassNotFoundData() '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.', + '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: Symfony\Component\Debug\Exception\UndefinedFunctionException.', ), ); }