summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArnold Daniels <arnold@jasny.net>2016-10-29 00:39:31 +0200
committerArnold Daniels <arnold@jasny.net>2016-10-29 00:39:31 +0200
commit763eae6f1e92a6f3b9b94875bec2f19979154da7 (patch)
tree24e2453d089efba94c18cb0de533b452462943b4
parentf5eb8869ae3f60b0e44cba7e273fd2ed5c63e1a8 (diff)
downloaderror-handler-763eae6f1e92a6f3b9b94875bec2f19979154da7.zip
error-handler-763eae6f1e92a6f3b9b94875bec2f19979154da7.tar.gz
error-handler-763eae6f1e92a6f3b9b94875bec2f19979154da7.tar.bz2
Error handler for uncaught execptions
-rw-r--r--src/ErrorHandler.php52
-rw-r--r--tests/ErrorHandlerTest.php188
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;