diff options
author | Arnold Daniels <arnold@jasny.net> | 2016-10-29 00:39:31 +0200 |
---|---|---|
committer | Arnold Daniels <arnold@jasny.net> | 2016-10-29 00:39:31 +0200 |
commit | 763eae6f1e92a6f3b9b94875bec2f19979154da7 (patch) | |
tree | 24e2453d089efba94c18cb0de533b452462943b4 | |
parent | f5eb8869ae3f60b0e44cba7e273fd2ed5c63e1a8 (diff) | |
download | error-handler-763eae6f1e92a6f3b9b94875bec2f19979154da7.zip error-handler-763eae6f1e92a6f3b9b94875bec2f19979154da7.tar.gz error-handler-763eae6f1e92a6f3b9b94875bec2f19979154da7.tar.bz2 |
Error handler for uncaught execptions
-rw-r--r-- | src/ErrorHandler.php | 52 | ||||
-rw-r--r-- | tests/ErrorHandlerTest.php | 188 |
2 files changed, 209 insertions, 31 deletions
diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 34c1f67..b6cb3a8 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -48,10 +48,10 @@ class ErrorHandler implements LoggerAwareInterface protected $logErrorTypes = 0; /** - * Log the following error types (in addition to caugth errors) - * @var int + * Log the following exception classes (and subclasses) + * @var array */ - protected $logExceptions = []; + protected $logExceptionClasses = []; /** * A string which reserves memory that can be used to log the error in case of an out of memory fatal error @@ -112,7 +112,6 @@ class ErrorHandler implements LoggerAwareInterface return $this->chainedExceptionHandler; } - /** * Get the types of errors that will be logged * @@ -124,6 +123,16 @@ class ErrorHandler implements LoggerAwareInterface } /** + * Get a list of Exception and other Throwable classes that will be logged + * @return array + */ + public function getLoggedExceptionClasses() + { + return $this->logExceptionClasses; + } + + + /** * Use the global error handler to convert E_USER_ERROR and E_RECOVERABLE_ERROR to an ErrorException */ public function converErrorsToExceptions() @@ -155,8 +164,8 @@ class ErrorHandler implements LoggerAwareInterface */ protected function logUncaughtException($class) { - if (!in_array($class, $this->logExceptions)) { - $this->logExceptions[] = $class; + if (!in_array($class, $this->logExceptionClasses)) { + $this->logExceptionClasses[] = $class; } $this->initExceptionHandler(); @@ -262,36 +271,41 @@ class ErrorHandler implements LoggerAwareInterface } /** - * Uncaught error handler + * Uncaught exception handler * @ignore * * @param \Exception|\Error $exception */ public function handleException($exception) { + $this->setExceptionHandler(null); + $this->setErrorHandler(null); + $isInstanceOf = array_map(function($class) use ($exception) { return is_a($exception, $class); - }, $this->logExceptions); + }, $this->logExceptionClasses); - $shouldLog = array_sum($isInstanceOf) > 0; + if ($exception instanceof \Error || $exception instanceof \ErrorException) { + $type = $exception instanceof \Error ? $exception->getCode() : $exception->getSeverity(); + $shouldLog = $this->logErrorTypes & $type; + } else { + $shouldLog = array_sum($isInstanceOf) > 0; + } if ($shouldLog) { $this->log($exception); } - if ($this->chainedExceptionHandler) { - call_user_func($this->chainedErrorHandler, $type, $message, $file, $line, $context); + if ($this->onFatalError) { + call_user_func($this->onFatalError, $exception); } - set_error_handler(null); + if ($this->chainedExceptionHandler) { + call_user_func($this->chainedExceptionHandler, $exception); + } - $warning = sprintf("Uncaught exception '%s' with message '%s' in %s:%d", get_class($exception), - $exception->getMessage(), $exception->getFile(), $exception->getLine()); - trigger_error($warning, E_USER_WARNING); - if ($this->onFatalError) { - call_user_func($this->onFatalError, $exception); - } + throw $exception; // This is now handled by the default exception and error handler } @@ -398,7 +412,7 @@ class ErrorHandler implements LoggerAwareInterface */ protected function setExceptionHandler($callback) { - return set_error_handler($callback); + return set_exception_handler($callback); } /** diff --git a/tests/ErrorHandlerTest.php b/tests/ErrorHandlerTest.php index ae4a611..fdd3f74 100644 --- a/tests/ErrorHandlerTest.php +++ b/tests/ErrorHandlerTest.php @@ -161,7 +161,7 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } - public function logUncaughtProvider() + public function logUncaughtErrorProvider() { return [ [E_ALL, $this->once(), $this->once()], @@ -177,13 +177,13 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } /** - * @dataProvider logUncaughtProvider + * @dataProvider logUncaughtErrorProvider * * @param int $code * @param InvokedCount $expectErrorHandler * @param InvokedCount $expectShutdownFunction */ - public function testLogUncaught($code, InvokedCount $expectErrorHandler, InvokedCount $expectShutdownFunction) + public function testLogUncaughtError($code, InvokedCount $expectErrorHandler, InvokedCount $expectShutdownFunction) { $errorHandler = $this->errorHandler; @@ -193,12 +193,30 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $errorHandler->expects($expectShutdownFunction)->method('registerShutdownFunction') ->with([$errorHandler, 'shutdownFunction']); + + $errorHandler->expects($this->never())->method('setExceptionHandler'); $errorHandler->logUncaught($code); $this->assertSame($code, $errorHandler->getLoggedErrorTypes()); } + public function testLogUncaughtException() + { + $errorHandler = $this->errorHandler; + + $errorHandler->expects($this->never())->method('setErrorHandler'); + $errorHandler->expects($this->never())->method('registerShutdownFunction'); + + $errorHandler->expects($this->once())->method('setExceptionHandler') + ->with([$errorHandler, 'handleException']) + ->willReturn(null); + + $errorHandler->logUncaught(\Exception::class); + + $this->assertSame([\Exception::class], $errorHandler->getLoggedExceptionClasses()); + } + public function testLogUncaughtCombine() { $errorHandler = $this->errorHandler; @@ -206,11 +224,30 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $errorHandler->logUncaught(E_NOTICE | E_USER_NOTICE); $errorHandler->logUncaught(E_WARNING | E_USER_WARNING); $errorHandler->logUncaught(E_ERROR); - $errorHandler->logUncaught(E_PARSE); + $errorHandler->logUncaught(E_ERROR | E_PARSE); + + $errorHandler->logUncaught(\LogicException::class); + $errorHandler->logUncaught(\UnderflowException::class); - $expected = E_NOTICE | E_USER_NOTICE | E_WARNING | E_USER_WARNING | E_ERROR | E_PARSE; - $this->assertSame($expected, $errorHandler->getLoggedErrorTypes()); + $this->assertSame( + E_NOTICE | E_USER_NOTICE | E_WARNING | E_USER_WARNING | E_ERROR | E_PARSE, + $errorHandler->getLoggedErrorTypes() + ); + + $this->assertSame( + [\LogicException::class, \UnderflowException::class], + $errorHandler->getLoggedExceptionClasses() + ); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testLogUncaughtInvalid() + { + $this->errorHandler->logUncaught([]); } + public function testInitErrorHandler() { @@ -230,6 +267,24 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $this->assertSame($callback, $errorHandler->getChainedErrorHandler()); } + public function testInitExceptionHandler() + { + $errorHandler = $this->errorHandler; + + $callback = function() {}; + + $errorHandler->expects($this->once())->method('setExceptionHandler') + ->with([$errorHandler, 'handleException']) + ->willReturn($callback); + + $errorHandler->logUncaught(\Exception::class); + + // Subsequent calls should have no effect + $errorHandler->logUncaught(\Exception::class); + + $this->assertSame($callback, $errorHandler->getChainedExceptionHandler()); + } + public function testInitShutdownFunction() { $errorHandler = $this->errorHandler; @@ -246,7 +301,7 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } - public function errorHandlerProvider() + public function handleErrorProvider() { return [ [0, E_WARNING, $this->never(), false], @@ -270,7 +325,7 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } /** - * @dataProvider errorHandlerProvider + * @dataProvider handleErrorProvider * * @param int $logUncaught * @param int $code @@ -292,7 +347,7 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } /** - * @dataProvider errorHandlerProvider + * @dataProvider handleErrorProvider * * @param int $logUncaught Ignored * @param int $code @@ -329,6 +384,78 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } } + public function handleExceptionProvider() + { + return [ + [[\Exception::class], new \Exception('no good'), $this->once()], + [[\Exception::class], new \LogicException('no good'), $this->once()], + [[\LogicException::class, \RuntimeException::class], new \Exception('no good'), $this->never()], + [[], new \Exception('no good'), $this->never()], + [[E_USER_ERROR], new \ErrorException('no good', null, E_USER_ERROR), $this->once()], + [[E_ERROR], new \ErrorException('no good', null, E_USER_ERROR), $this->never()], + ]; + } + + /** + * @dataProvider handleExceptionProvider + * + * @param array $logUncaught + * @param \Exception $exception + * @param InvokedCount $expectLog + */ + public function testHandleException(array $logUncaught, \Exception $exception, InvokedCount $expectLog) + { + $class = get_class($exception); + + if ($exception instanceof \ErrorException) { + $message = 'Fatal error: no good'; + } else { + $message = sprintf('Uncaught Exception %s: "%s"', $class, "no good"); + } + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($expectLog)->method('log') + ->with($this->isType('string'), $this->stringStartsWith($message), $this->anything()); + + $errorHandler = $this->errorHandler; + + $errorHandler->setLogger($logger); + + foreach ($logUncaught as $exceptionClass) { + $errorHandler->logUncaught($exceptionClass); + } + + $this->expectException($class); + $this->expectExceptionMessage('no good'); + + $errorHandler->handleException($exception); + } + + public function testHandleExceptionChaining() + { + $exception = new \Exception('no good'); + + $handler = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $handler->expects($this->once())->method('__invoke') + ->with($exception); + + $errorHandler = $this->errorHandler; + + $errorHandler->expects($this->exactly(2))->method('setExceptionHandler') + ->withConsecutive([[$errorHandler, 'handleException']], [null]) + ->willReturnOnConsecutiveCalls($handler, [$errorHandler, 'handleException']); + + $errorHandler->expects($this->once())->method('setErrorHandler')->with(null); + + $errorHandler->logUncaught(\Exception::class); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('no good'); + + $errorHandler->handleException($exception); + } + + public function shutdownFunctionProvider() { return [ @@ -376,7 +503,8 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $this->assertAttributeEmpty('reservedMemory', $errorHandler); } - public function shutdownFunctionWithCallbackProvider() + + public function onFatalErrorProvider() { return [ [true, $this->once()], @@ -385,12 +513,48 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } /** - * @dataProvider shutdownFunctionWithCallbackProvider + * @dataProvider onFatalErrorProvider + * + * @param boolean $clearOutput + * @param InvokedCount $expectClear + */ + public function testOnFatalErrorFromHandleException($clearOutput, InvokedCount $expectClear) + { + $exception = new \Exception('no good'); + + $handler = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $handler->expects($this->once())->method('__invoke') + ->with($exception); + + $errorHandler = $this->errorHandler; + + $errorHandler->expects($this->exactly(2))->method('setExceptionHandler') + ->withConsecutive([[$errorHandler, 'handleException']], [null]) + ->willReturnOnConsecutiveCalls($handler, [$errorHandler, 'handleException']); + + $errorHandler->expects($expectClear)->method('clearOutputBuffer'); + + $callback = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $callback->expects($this->once())->method('__invoke') + ->with($exception); + + $errorHandler->onFatalError($callback, $clearOutput); + + $errorHandler->logUncaught(\Exception::class); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('no good'); + + $errorHandler->handleException($exception); + } + + /** + * @dataProvider onFatalErrorProvider * * @param boolean $clearOutput * @param InvokedCount $expectClear */ - public function testShutdownFunctionWithCallback($clearOutput, InvokedCount $expectClear) + public function testOnFatalErrorFromShutdownFunction($clearOutput, InvokedCount $expectClear) { $errorHandler = $this->errorHandler; |