diff options
author | Arnold Daniels <arnold@jasny.net> | 2016-10-28 02:02:29 +0200 |
---|---|---|
committer | Arnold Daniels <arnold@jasny.net> | 2016-10-28 02:02:29 +0200 |
commit | f5eb8869ae3f60b0e44cba7e273fd2ed5c63e1a8 (patch) | |
tree | 744749d657e031d9dbef085784bc293312bba55d | |
parent | 1ef376a930f400a8173e3cb8bfb08923622bcfe5 (diff) | |
download | error-handler-f5eb8869ae3f60b0e44cba7e273fd2ed5c63e1a8.zip error-handler-f5eb8869ae3f60b0e44cba7e273fd2ed5c63e1a8.tar.gz error-handler-f5eb8869ae3f60b0e44cba7e273fd2ed5c63e1a8.tar.bz2 |
Make using Error Handler as PSR-7 middleware optional
Allow handling uncaught Exceptions (WIP)
-rw-r--r-- | README.md | 100 | ||||
-rw-r--r-- | composer.json | 2 | ||||
-rw-r--r-- | src/ErrorHandler.php | 352 | ||||
-rw-r--r-- | src/ErrorHandler/ErrorCodes.php | 84 | ||||
-rw-r--r-- | src/ErrorHandler/Logging.php | 104 | ||||
-rw-r--r-- | src/ErrorHandler/Middleware.php | 76 | ||||
-rw-r--r-- | tests/ErrorHandler/MiddlewareTest.php | 143 | ||||
-rw-r--r-- | tests/ErrorHandlerTest.php | 184 |
8 files changed, 638 insertions, 407 deletions
@@ -5,21 +5,7 @@ Jasny Error Handler [](https://scrutinizer-ci.com/g/jasny/error-handler/?branch=master) [](https://scrutinizer-ci.com/g/jasny/error-handler/?branch=master) -Error handler as PSR-7 compatible middleware. - -The error will catch [Exceptions](http://php.net/manual/en/class.exception.php) and -[Errors](http://php.net/manual/en/class.error.php). - -You can use this middleware with: - -* [Jasny Router](https://github.com/jasny/router) -* [Relay](https://github.com/relayphp/Relay.Relay) -* [Expressive](http://framework.zend.com/expressive) -* [Slim 3](http://www.slimframework.com) - -You can log with a [PSR-3 compatible logger](http://www.php-fig.org/psr/psr-3/) like -[Monolog](https://github.com/Seldaek/monolog). - +Error handler with PSR-7 support. Installation --- @@ -27,32 +13,62 @@ Installation The Jasny Error Handler package is available on [packagist](https://packagist.org/packages/jasny/error-handler). Install it using composer: - composer require jasny/error-handler-middleware + composer require jasny/error-handler Usage --- ```php -use Jasny\ErrorHandler; +$errorHandler = new Jasny\ErrorHandler(); +``` + +Just creating an error handler will do nothing. You can use it for logging, handling fatal errors and as PSR-7 compatible +middleware. + +## Logging + +By default the error handler with only catch [Throwables](http://php.net/manual/en/class.throwable.php) and not set the +[php error handler](http://php.net/set_error_handler). + +To log these errors, set the logger using the `setLogger()` method. You can log with any +[PSR-3 compatible logger](http://www.php-fig.org/psr/psr-3/) like [Monolog](https://github.com/Seldaek/monolog). + +The `logUncaught()` method will set the error handler, so warnings and notices can be logged. It may also [register a +shutdown function](http://php.net/manual/en/function.register-shutdown-function.php) to handle uncatchable fatal +errors. + +```php use Monolog\Logger; use Monolog\Handler\StreamHandler; +$errorHandler = new Jasny\ErrorHandler(); + $log = new Logger('test'); $log->pushHandler(new StreamHandler('path/to/your.log')); -$errorHandler = new ErrorHandler(); - -// Log errors +// Log fatal errors, warnings and uncaught exceptions $errorHandler->setLogger($log); -// Log fatal errors and warnings in addition to catchable errors -$errorHandler->alsoLog(E_PARSE | E_ERROR | E_WARNING | E_USER_WARNING); - -// PHP 5 support -$errorHandler->converErrorsToExceptions(); +$errorHandler->logUncaught(E_PARSE | E_ERROR | E_WARNING | E_USER_WARNING); +$errorHandler->logUncaught(Exception::class); +$errorHandler->logUncaught(Error::class); // PHP7 only ``` +## PSR-7 compatible middleware + +The error handler can be used as PSR-7 compatible middleware. + +The error will catch [Exceptions](http://php.net/manual/en/class.exception.php) and +[Errors](http://php.net/manual/en/class.error.php). + +You can use this middleware with: + +* [Jasny Router](https://github.com/jasny/router) +* [Relay](https://github.com/relayphp/Relay.Relay) +* [Expressive](http://framework.zend.com/expressive) +* [Slim 3](http://www.slimframework.com) + For example use it with **Relay**: ```php @@ -60,8 +76,10 @@ use Relay\RelayBuilder; use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\Response; +$errorHandler = new Jasny\ErrorHandler(); + $relay = new RelayBuilder(); -$dispatcher = $relay->newInstance([$errorHandler]); +$dispatcher = $relay->newInstance([$errorHandler->asMiddleware()]); $response = $dispatcher((new ServerRequest())->withGlobalEnvironment(), new Response()); ``` @@ -74,42 +92,34 @@ use Jasny\Router\Routes; use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\Response; +$errorHandler = new Jasny\ErrorHandler(); + $routes = new Routes\Glob(['/**' => ['controller' => '$1', 'id' => '$2']); $router = new Router($routes); -$router->add($errorHandler); +$router->add($errorHandler->asMiddleware()); $response = $dispatcher((new ServerRequest())->withGlobalEnvironment(), new Response()); ``` -## Logging - -By default the error handler with only catch [Throwables](http://php.net/manual/en/class.throwable.php) and not set the -[php error handler](http://php.net/set_error_handler). +### PHP 5 support -To log these errors, set the logger using the `setLogger()` method. - -The `alsoLog()` method will set the error handler, so warnings and notices can be logged. It may also [register a -shutdown function](http://php.net/manual/en/function.register-shutdown-function.php) to handle uncatchable fatal -errors. - -## PHP 5 support - -To add support for PHP5, you should call `converErrorsToExceptions()`. This method will convert an error to an +With PHP 5 errors aren't thrown, so the middleware won't handle it. To add middleware support for errors in PHP5, you +should call `converErrorsToExceptions()`. This method will convert an error to an [ErrorException](http://php.net/manual/en/class.errorexception.php). ## Handling fatal errors -Some errors, like syntax errors, are not thrown and can't be handled be a user defined error handler. The PHP script -will always exit right away when such an error occurs. +Errors that are not thrown, like syntax errors, are not caught, will cause a fatal error. These errors will be logged +by the error when specified with the `logUncaught()` method. -By [registering a shutdown function](http://php.net/manual/en/function.register-shutdown-function.php) we can act upon -these errors. Using `alsoLog(E_ERROR | E_PARSE)` will cause these errors to be logged. With the `onFatalError()` method -you take additional action, like output an error message. +With the `onFatalError()` method you take additional action, like output a pretty error message. ```php ob_start(); +$errorHandler = new Jasny\ErrorHandler(); + $errorHandler->onFatalError(function() { http_response_code(500); header('Content-Type: text/html'); diff --git a/composer.json b/composer.json index c7ee8f8..1e1e412 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "jasny/error-handler", - "description": "Error handler as PSR-7 compatible middleware", + "description": "Error handler with PSR-7 support", "keywords": ["error handler", "exception handler", "middleware"], "license": "MIT", "authors": [ diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 0737322..34c1f67 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -2,23 +2,19 @@ namespace Jasny; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Log\LoggerInterface; +use Jasny\ErrorHandler\ErrorCodes; +use Jasny\ErrorHandler\Logging; +use Jasny\ErrorHandler\Middleware; use Psr\Log\LoggerAwareInterface; -use Psr\Log\LogLevel; -use Psr\Log\NullLogger; /** * Handle error in following middlewares/app actions */ class ErrorHandler implements LoggerAwareInterface { - /** - * @var LoggerInterface - */ - protected $logger; - + use Logging; + use ErrorCodes; + /** * @var \Exception|\Error */ @@ -30,6 +26,11 @@ class ErrorHandler implements LoggerAwareInterface protected $chainedErrorHandler; /** + * @var callable|false + */ + protected $chainedExceptionHandler; + + /** * @var boolean */ protected $registeredShutdown = false; @@ -47,6 +48,12 @@ class ErrorHandler implements LoggerAwareInterface protected $logErrorTypes = 0; /** + * Log the following error types (in addition to caugth errors) + * @var int + */ + protected $logExceptions = []; + + /** * A string which reserves memory that can be used to log the error in case of an out of memory fatal error * @var string */ @@ -59,98 +66,40 @@ class ErrorHandler implements LoggerAwareInterface /** - * Set the logger for logging errors - * - * @param LoggerInterface $logger - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * Set the logger for logging errors + * Set the caught error * - * @return LoggerInterface + * @param \Throwable|\Exception|\Error */ - public function getLogger() + public function setError($error) { - if (!isset($this->logger)) { - $this->logger = new NullLogger(); - } - - return $this->logger; - } - - /** - * Log an error or exception - * - * @param \Exception|\Error $error - */ - public function log($error) - { - if ($error instanceof \Error || $error instanceof \ErrorException) { - return $this->logError($error); - } - - if ($error instanceof \Exception) { - return $this->logException($error); + if (!$error instanceof \Error && !$error instanceof \Exception) { + $type = (is_object($error) ? get_class($error) . ' ' : '') . gettype($error); + trigger_error("Excpeted an Error or Exception, got a $type", E_USER_WARNING); + return; } - $message = "Unable to log a " . (is_object($error) ? get_class($error) . ' ' : '') . gettype($error); - $this->getLogger()->log(LogLevel::WARNING, $message); - } - - /** - * Log an error - * - * @param \Error|\ErrorException $error - */ - protected function logError($error) - { - $code = $error instanceof \ErrorException ? $error->getSeverity() : E_ERROR; - $level = $this->getLogLevel($code); - - $message = sprintf('%s: %s at %s line %s', $this->codeToString($code), $error->getMessage(), - $error->getFile(), $error->getLine()); - - $context = [ - 'error' => $error, - 'code' => $code, - 'message' => $error->getMessage(), - 'file' => $error->getFile(), - 'line' => $error->getLine() - ]; - - $this->getLogger()->log($level, $message, $context); + $this->error = $error; } /** - * Log an exception + * Get the caught error * - * @param \Exception $error + * @return \Throwable|\Exception|\Error */ - protected function logException(\Exception $error) + public function getError() { - $level = $this->getLogLevel(); - - $message = sprintf('Uncaught Exception %s: "%s" at %s line %s', get_class($error), $error->getMessage(), - $error->getFile(), $error->getLine()); - - $context = ['exception' => $error]; - - $this->getLogger()->log($level, $message, $context); + return $this->error; } /** - * Get the caught error + * Get the error handler that has been replaced. * - * @return \Throwable|\Exception|\Error + * @return callable|false|null */ - public function getError() + public function getChainedErrorHandler() { - return $this->error; + return $this->chainedErrorHandler; } /** @@ -158,11 +107,12 @@ class ErrorHandler implements LoggerAwareInterface * * @return callable|false|null */ - public function getChainedErrorHandler() + public function getChainedExceptionHandler() { - return $this->chainedErrorHandler; + return $this->chainedExceptionHandler; } + /** * Get the types of errors that will be logged * @@ -173,83 +123,65 @@ class ErrorHandler implements LoggerAwareInterface return $this->logErrorTypes; } - /** - * Run middleware action - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param callback $next - * @return ResponseInterface + * Use the global error handler to convert E_USER_ERROR and E_RECOVERABLE_ERROR to an ErrorException */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + public function converErrorsToExceptions() { - if (!is_callable($next)) { - throw new \InvalidArgumentException("'next' should be a callback"); - } - - try { - $this->error = null; - $nextResponse = $next($request, $response); - } catch (\Error $e) { - $this->error = $e; - } catch (\Exception $e) { - $this->error = $e; - } - - if ($this->error) { - $this->log($this->error); - $nextResponse = $this->errorResponse($request, $response); - } - - return $nextResponse; + $this->convertFatalErrors = true; + $this->initErrorHandler(); } - + /** - * Handle caught error - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @return ResponseInterface + * Log these types of errors or exceptions + * + * @param int|string $type E_* contants as binary set OR Exception class name */ - protected function errorResponse(ServerRequestInterface $request, ResponseInterface $response) + public function logUncaught($type) { - $errorResponse = $response->withStatus(500); - $errorResponse->getBody()->write('Unexpected error'); - - return $errorResponse; + if (is_int($type)) { + $this->logUncaughtErrors($type); + } elseif (is_string($type)) { + $this->logUncaughtException($type); + } else { + throw new \InvalidArgumentException("Type should be an error code (int) or Exception class (string)"); + } } - /** - * Use the global error handler to convert E_USER_ERROR and E_RECOVERABLE_ERROR to an ErrorException + * Log these types of errors or exceptions + * + * @param string $class Exception class name */ - public function converErrorsToExceptions() + protected function logUncaughtException($class) { - $this->convertFatalErrors = true; - $this->initErrorHandler(); + if (!in_array($class, $this->logExceptions)) { + $this->logExceptions[] = $class; + } + + $this->initExceptionHandler(); } /** - * Also log these types of errors in addition to caught errors and exceptions + * Log these types of errors or exceptions * - * @param int $errorTypes E_* contants as binary set + * @param int $type E_* contants as binary set */ - public function alsoLog($errorTypes) + protected function logUncaughtErrors($type) { - $this->logErrorTypes |= $errorTypes; - - $nonFatal = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_STRICT | E_DEPRECATED | E_USER_DEPRECATED; + $this->logErrorTypes |= $type; + $unhandled = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR; - - if ($this->logErrorTypes & $nonFatal) { + + if ($type & ~$unhandled) { $this->initErrorHandler(); } - - if ($this->logErrorTypes & $unhandled) { + + if ($type & $unhandled) { $this->initShutdownFunction(); } } + /** * Set a callback for when the script dies because of a fatal, non-catchable error. @@ -271,6 +203,15 @@ class ErrorHandler implements LoggerAwareInterface } /** + * Use this error handler as middleware + */ + public function asMiddleware() + { + return new Middleware($this); + } + + + /** * Use the global error handler */ protected function initErrorHandler() @@ -309,6 +250,51 @@ class ErrorHandler implements LoggerAwareInterface : false; } + + /** + * Use the global error handler + */ + protected function initExceptionHandler() + { + if (!isset($this->chainedExceptionHandler)) { + $this->chainedExceptionHandler = $this->setExceptionHandler([$this, 'handleException']) ?: false; + } + } + + /** + * Uncaught error handler + * @ignore + * + * @param \Exception|\Error $exception + */ + public function handleException($exception) + { + $isInstanceOf = array_map(function($class) use ($exception) { + return is_a($exception, $class); + }, $this->logExceptions); + + $shouldLog = array_sum($isInstanceOf) > 0; + + if ($shouldLog) { + $this->log($exception); + } + + if ($this->chainedExceptionHandler) { + call_user_func($this->chainedErrorHandler, $type, $message, $file, $line, $context); + } + + set_error_handler(null); + + $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); + } + } + + /** * Reserve memory for shutdown function in case of out of memory */ @@ -356,81 +342,7 @@ class ErrorHandler implements LoggerAwareInterface } } - - /** - * Get the log level for an error code - * - * @param int $code E_* error code - * @return string - */ - protected function getLogLevel($code = null) - { - switch ($code) { - case E_STRICT: - case E_DEPRECATED: - case E_USER_DEPRECATED: - return LogLevel::INFO; - - case E_NOTICE: - case E_USER_NOTICE: - return LogLevel::NOTICE; - - case E_WARNING: - case E_CORE_WARNING: - case E_COMPILE_WARNING: - case E_USER_WARNING: - return LogLevel::WARNING; - - case E_PARSE: - case E_CORE_ERROR: - case E_COMPILE_ERROR: - return LogLevel::CRITICAL; - - default: - return LogLevel::ERROR; - } - } - - /** - * Turn an error code into a string - * - * @param int $code - * @return string - */ - protected function codeToString($code) - { - switch ($code) { - case E_ERROR: - case E_USER_ERROR: - case E_RECOVERABLE_ERROR: - return 'Fatal error'; - case E_WARNING: - case E_USER_WARNING: - return 'Warning'; - case E_PARSE: - return 'Parse error'; - case E_NOTICE: - case E_USER_NOTICE: - return 'Notice'; - case E_CORE_ERROR: - return 'Core error'; - case E_CORE_WARNING: - return 'Core warning'; - case E_COMPILE_ERROR: - return 'Compile error'; - case E_COMPILE_WARNING: - return 'Compile warning'; - case E_STRICT: - return 'Strict standards'; - case E_DEPRECATED: - case E_USER_DEPRECATED: - return 'Deprecated'; - } - - return 'Unknown error'; - } - - + /** * Clear and destroy all the output buffers * @codeCoverageIgnore @@ -478,6 +390,18 @@ class ErrorHandler implements LoggerAwareInterface } /** + * Wrapper method for `set_exception_handler` + * @codeCoverageIgnore + * + * @param callable $callback + * @return callable|null + */ + protected function setExceptionHandler($callback) + { + return set_error_handler($callback); + } + + /** * Wrapper method for `register_shutdown_function` * @codeCoverageIgnore * diff --git a/src/ErrorHandler/ErrorCodes.php b/src/ErrorHandler/ErrorCodes.php new file mode 100644 index 0000000..6ef2957 --- /dev/null +++ b/src/ErrorHandler/ErrorCodes.php @@ -0,0 +1,84 @@ +<?php + +namespace Jasny\ErrorHandler; + +use Psr\Log\LogLevel; + +/** + * Trait for using E_* error codes + */ +trait ErrorCodes +{ + /** + * Get the log level for an error code + * + * @param int $code E_* error code + * @return string + */ + protected function getLogLevel($code = null) + { + switch ($code) { + case E_STRICT: + case E_DEPRECATED: + case E_USER_DEPRECATED: + return LogLevel::INFO; + + case E_NOTICE: + case E_USER_NOTICE: + return LogLevel::NOTICE; + + case E_WARNING: + case E_CORE_WARNING: + case E_COMPILE_WARNING: + case E_USER_WARNING: + return LogLevel::WARNING; + + case E_PARSE: + case E_CORE_ERROR: + case E_COMPILE_ERROR: + return LogLevel::CRITICAL; + + default: + return LogLevel::ERROR; + } + } + + /** + * Turn an error code into a string + * + * @param int $code + * @return string + */ + protected function codeToString($code) + { + switch ($code) { + case E_ERROR: + case E_USER_ERROR: + case E_RECOVERABLE_ERROR: + return 'Fatal error'; + case E_WARNING: + case E_USER_WARNING: + return 'Warning'; + case E_PARSE: + return 'Parse error'; + case E_NOTICE: + case E_USER_NOTICE: + return 'Notice'; + case E_CORE_ERROR: + return 'Core error'; + case E_CORE_WARNING: + return 'Core warning'; + case E_COMPILE_ERROR: + return 'Compile error'; + case E_COMPILE_WARNING: + return 'Compile warning'; + case E_STRICT: + return 'Strict standards'; + case E_DEPRECATED: + case E_USER_DEPRECATED: + return 'Deprecated'; + } + + return 'Unknown error'; + } +}
\ No newline at end of file diff --git a/src/ErrorHandler/Logging.php b/src/ErrorHandler/Logging.php new file mode 100644 index 0000000..6d531bd --- /dev/null +++ b/src/ErrorHandler/Logging.php @@ -0,0 +1,104 @@ +<?php + +namespace Jasny\ErrorHandler; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Psr\Log\NullLogger; + +/** + * Trait for logging errors and exceptions + */ +trait Logging +{ + /** + * @var LoggerInterface + */ + protected $logger; + + + /** + * Set the logger for logging errors + * + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Set the logger for logging errors + * + * @return LoggerInterface + */ + public function getLogger() + { + if (!isset($this->logger)) { + $this->logger = new NullLogger(); + } + + return $this->logger; + } + + + /** + * Log an error or exception + * + * @param \Exception|\Error $error + */ + public function log($error) + { + if ($error instanceof \Error || $error instanceof \ErrorException) { + return $this->logError($error); + } + + if ($error instanceof \Exception) { + return $this->logException($error); + } + + $message = "Unable to log a " . (is_object($error) ? get_class($error) . ' ' : '') . gettype($error); + $this->getLogger()->log(LogLevel::WARNING, $message); + } + + /** + * Log an error + * + * @param \Error|\ErrorException $error + */ + protected function logError($error) + { + $code = $error instanceof \ErrorException ? $error->getSeverity() : E_ERROR; + $level = $this->getLogLevel($code); + + $message = sprintf('%s: %s at %s line %s', $this->codeToString($code), $error->getMessage(), + $error->getFile(), $error->getLine()); + + $context = [ + 'error' => $error, + 'code' => $code, + 'message' => $error->getMessage(), + 'file' => $error->getFile(), + 'line' => $error->getLine() + ]; + + $this->getLogger()->log($level, $message, $context); + } + + /** + * Log an exception + * + * @param \Exception $exception + */ + protected function logException(\Exception $exception) + { + $level = $this->getLogLevel(); + + $message = sprintf('Uncaught Exception %s: "%s" at %s line %s', get_class($exception), $exception->getMessage(), + $exception->getFile(), $exception->getLine()); + + $context = compact('exception'); + + $this->getLogger()->log($level, $message, $context); + } +} diff --git a/src/ErrorHandler/Middleware.php b/src/ErrorHandler/Middleware.php new file mode 100644 index 0000000..710f6e7 --- /dev/null +++ b/src/ErrorHandler/Middleware.php @@ -0,0 +1,76 @@ +<?php + +namespace Jasny\ErrorHandler; + +use Jasny\ErrorHandler; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * Use error handler as middleware + */ +class Middleware +{ + /** + * @var ErrorHandler + */ + protected $errorHandler; + + /** + * Class constructor + * + * @param ErrorHandler $errorHandler + */ + public function __construct(ErrorHandler $errorHandler) + { + $this->errorHandler = $errorHandler; + } + + /** + * Run middleware action + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callback $next + * @return ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + { + if (!is_callable($next)) { + throw new \InvalidArgumentException("'next' should be a callback"); + } + + try { + $nextResponse = $next($request, $response); + $error = null; + } catch (\Error $e) { + $error = $e; + } catch (\Exception $e) { + $error = $e; + } + + $this->errorHandler->setError($error); + + if ($error) { + $this->errorHandler->log($error); + $nextResponse = $this->errorResponse($request, $response); + } + + return $nextResponse; + } + + /** + * Handle caught error + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return ResponseInterface + */ + protected function errorResponse(ServerRequestInterface $request, ResponseInterface $response) + { + $errorResponse = $response->withStatus(500); + $errorResponse->getBody()->write('Unexpected error'); + + return $errorResponse; + } +} diff --git a/tests/ErrorHandler/MiddlewareTest.php b/tests/ErrorHandler/MiddlewareTest.php new file mode 100644 index 0000000..d16b239 --- /dev/null +++ b/tests/ErrorHandler/MiddlewareTest.php @@ -0,0 +1,143 @@ +<?php + +namespace Jasny\ErrorHandler; + +/** + * Description of MiddlewareTest + * + * @author arnold + */ +class MiddlewareTest +{ + + /** + * Test invoke with invalid 'next' param + * + * @expectedException \InvalidArgumentException + */ + public function testInvokeInvalidNext() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $errorHandler = $this->errorHandler; + + $errorHandler($request, $response, 'not callable'); + } + + /** + * Test case when there is no error + */ + public function testInvokeNoError() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $finalResponse = $this->createMock(ResponseInterface::class); + + $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $next->expects($this->once())->method('__invoke') + ->with($request, $response) + ->willReturn($finalResponse); + + $errorHandler = $this->errorHandler; + + $result = $errorHandler($request, $response, $next); + + $this->assertSame($finalResponse, $result); + } + + /** + * Test that Exception in 'next' callback is caught + */ + public function testInvokeCatchException() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $errorResponse = $this->createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $exception = $this->createMock(\Exception::class); + + $stream->expects($this->once())->method('write')->with('Unexpected error'); + $response->expects($this->once())->method('withStatus')->with(500)->willReturn($errorResponse); + + $errorResponse->expects($this->once())->method('getBody')->willReturn($stream); + + $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $next->expects($this->once())->method('__invoke') + ->with($request, $response) + ->willThrowException($exception); + + $errorHandler = $this->errorHandler; + + $result = $errorHandler($request, $response, $next); + + $this->assertSame($errorResponse, $result); + $this->assertSame($exception, $errorHandler->getError()); + } + + /** + * Test that an error in 'next' callback is caught + */ + public function testInvokeCatchError() + { + if (!class_exists('Error')) { + $this->markTestSkipped(PHP_VERSION . " doesn't throw errors yet"); + } + + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $errorResponse = $this->createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $stream->expects($this->once())->method('write')->with('Unexpected error'); + $response->expects($this->once())->method('withStatus')->with(500)->willReturn($errorResponse); + + $errorResponse->expects($this->once())->method('getBody')->willReturn($stream); + + $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $next->expects($this->once())->method('__invoke') + ->with($request, $response) + ->willReturnCallback(function() { + \this_function_does_not_exist(); + }); + + $errorHandler = $this->errorHandler; + + $result = $errorHandler($request, $response, $next); + + $this->assertSame($errorResponse, $result); + + $error = $errorHandler->getError(); + $this->assertEquals("Call to undefined function this_function_does_not_exist()", $error->getMessage()); + } + + public function testInvokeLog() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $response->method('withStatus')->willReturnSelf(); + $response->method('getBody')->willReturn($stream); + + $exception = $this->createMock(\Exception::class); + + $message = $this->stringStartsWith('Uncaught Exception ' . get_class($exception)); + $context = ['exception' => $exception]; + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once())->method('log') + ->with(LogLevel::ERROR, $message, $context); + + $errorHandler = $this->errorHandler; + $errorHandler->setLogger($logger); + + $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $next->expects($this->once())->method('__invoke') + ->with($request, $response) + ->willThrowException($exception); + + $errorHandler($request, $response, $next); + } +}
\ No newline at end of file diff --git a/tests/ErrorHandlerTest.php b/tests/ErrorHandlerTest.php index a49b3ad..ae4a611 100644 --- a/tests/ErrorHandlerTest.php +++ b/tests/ErrorHandlerTest.php @@ -14,6 +14,8 @@ use PHPUnit_Framework_MockObject_Matcher_InvokedCount as InvokedCount; /** * @covers Jasny\ErrorHandler + * @covers Jasny\ErrorHandler\ErrorCodes + * @covers Jasny\ErrorHandler\Logging */ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase { @@ -25,113 +27,31 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->errorHandler = $this->getMockBuilder(ErrorHandler::class) - ->setMethods(['errorReporting', 'errorGetLast', 'setErrorHandler', 'registerShutdownFunction', - 'clearOutputBuffer']) + ->setMethods(['errorReporting', 'errorGetLast', 'setErrorHandler', 'setExceptionHandler', + 'registerShutdownFunction', 'clearOutputBuffer']) ->getMock(); } - /** - * Test invoke with invalid 'next' param - * - * @expectedException \InvalidArgumentException - */ - public function testInvokeInvalidNext() - { - $request = $this->createMock(ServerRequestInterface::class); - $response = $this->createMock(ResponseInterface::class); - - $errorHandler = $this->errorHandler; - - $errorHandler($request, $response, 'not callable'); - } - - /** - * Test case when there is no error - */ - public function testInvokeNoError() - { - $request = $this->createMock(ServerRequestInterface::class); - $response = $this->createMock(ResponseInterface::class); - $finalResponse = $this->createMock(ResponseInterface::class); - - $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); - $next->expects($this->once())->method('__invoke') - ->with($request, $response) - ->willReturn($finalResponse); - - $errorHandler = $this->errorHandler; - - $result = $errorHandler($request, $response, $next); - - $this->assertSame($finalResponse, $result); - } - /** - * Test that Exception in 'next' callback is caught - */ - public function testInvokeCatchException() + public function testSetError() { - $request = $this->createMock(ServerRequestInterface::class); - $response = $this->createMock(ResponseInterface::class); - $errorResponse = $this->createMock(ResponseInterface::class); - $stream = $this->createMock(StreamInterface::class); - - $exception = $this->createMock(\Exception::class); - - $stream->expects($this->once())->method('write')->with('Unexpected error'); - $response->expects($this->once())->method('withStatus')->with(500)->willReturn($errorResponse); - - $errorResponse->expects($this->once())->method('getBody')->willReturn($stream); + $exception = new \Exception(); - $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); - $next->expects($this->once())->method('__invoke') - ->with($request, $response) - ->willThrowException($exception); - - $errorHandler = $this->errorHandler; - - $result = $errorHandler($request, $response, $next); - - $this->assertSame($errorResponse, $result); - $this->assertSame($exception, $errorHandler->getError()); + $this->errorHandler->setError($exception); + $this->assertSame($exception, $this->errorHandler->getError()); } /** - * Test that an error in 'next' callback is caught + * @expectedException PHPUnit_Framework_Error_Warning */ - public function testInvokeCatchError() + public function testSetErrorWithInvalid() { - if (!class_exists('Error')) { - $this->markTestSkipped(PHP_VERSION . " doesn't throw errors yet"); - } + @$this->errorHandler->setError('foo'); + $this->assertNull($this->errorHandler->getError()); - $request = $this->createMock(ServerRequestInterface::class); - $response = $this->createMock(ResponseInterface::class); - $errorResponse = $this->createMock(ResponseInterface::class); - $stream = $this->createMock(StreamInterface::class); - - $stream->expects($this->once())->method('write')->with('Unexpected error'); - $response->expects($this->once())->method('withStatus')->with(500)->willReturn($errorResponse); - - $errorResponse->expects($this->once())->method('getBody')->willReturn($stream); - - $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); - $next->expects($this->once())->method('__invoke') - ->with($request, $response) - ->willReturnCallback(function() { - \this_function_does_not_exist(); - }); - - $errorHandler = $this->errorHandler; - - $result = $errorHandler($request, $response, $next); - - $this->assertSame($errorResponse, $result); - - $error = $errorHandler->getError(); - $this->assertEquals("Call to undefined function this_function_does_not_exist()", $error->getMessage()); + $this->errorHandler->setError('foo'); } - + public function testSetLogger() { @@ -143,36 +63,6 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $this->assertSame($logger, $errorHandler->getLogger()); } - - public function testInvokeLog() - { - $request = $this->createMock(ServerRequestInterface::class); - $response = $this->createMock(ResponseInterface::class); - $stream = $this->createMock(StreamInterface::class); - - $response->method('withStatus')->willReturnSelf(); - $response->method('getBody')->willReturn($stream); - - $exception = $this->createMock(\Exception::class); - - $message = $this->stringStartsWith('Uncaught Exception ' . get_class($exception)); - $context = ['exception' => $exception]; - - $logger = $this->createMock(LoggerInterface::class); - $logger->expects($this->once())->method('log') - ->with(LogLevel::ERROR, $message, $context); - - $errorHandler = $this->errorHandler; - $errorHandler->setLogger($logger); - - $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); - $next->expects($this->once())->method('__invoke') - ->with($request, $response) - ->willThrowException($exception); - - $errorHandler($request, $response, $next); - } - public function errorProvider() { return [ @@ -271,7 +161,7 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } - public function alsoLogProvider() + public function logUncaughtProvider() { return [ [E_ALL, $this->once(), $this->once()], @@ -281,19 +171,19 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase [E_DEPRECATED | E_USER_DEPRECATED, $this->once(), $this->never()], [E_PARSE, $this->never(), $this->once()], [E_ERROR, $this->never(), $this->once()], - [E_ERROR | E_USER_ERROR, $this->never(), $this->once()], - [E_RECOVERABLE_ERROR | E_USER_ERROR, $this->never(), $this->never()] + [E_ERROR | E_USER_ERROR, $this->once(), $this->once()], + [E_RECOVERABLE_ERROR | E_USER_ERROR, $this->once(), $this->never()] ]; } /** - * @dataProvider alsoLogProvider + * @dataProvider logUncaughtProvider * * @param int $code * @param InvokedCount $expectErrorHandler * @param InvokedCount $expectShutdownFunction */ - public function testAlsoLog($code, InvokedCount $expectErrorHandler, InvokedCount $expectShutdownFunction) + public function testLogUncaught($code, InvokedCount $expectErrorHandler, InvokedCount $expectShutdownFunction) { $errorHandler = $this->errorHandler; @@ -304,19 +194,19 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $errorHandler->expects($expectShutdownFunction)->method('registerShutdownFunction') ->with([$errorHandler, 'shutdownFunction']); - $errorHandler->alsoLog($code); + $errorHandler->logUncaught($code); $this->assertSame($code, $errorHandler->getLoggedErrorTypes()); } - public function testAlsoLogCombine() + public function testLogUncaughtCombine() { $errorHandler = $this->errorHandler; - $errorHandler->alsoLog(E_NOTICE | E_USER_NOTICE); - $errorHandler->alsoLog(E_WARNING | E_USER_WARNING); - $errorHandler->alsoLog(E_ERROR); - $errorHandler->alsoLog(E_PARSE); + $errorHandler->logUncaught(E_NOTICE | E_USER_NOTICE); + $errorHandler->logUncaught(E_WARNING | E_USER_WARNING); + $errorHandler->logUncaught(E_ERROR); + $errorHandler->logUncaught(E_PARSE); $expected = E_NOTICE | E_USER_NOTICE | E_WARNING | E_USER_WARNING | E_ERROR | E_PARSE; $this->assertSame($expected, $errorHandler->getLoggedErrorTypes()); @@ -332,10 +222,10 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase ->with([$errorHandler, 'handleError']) ->willReturn($callback); - $errorHandler->alsoLog(E_WARNING); + $errorHandler->logUncaught(E_WARNING); // Subsequent calls should have no effect - $errorHandler->alsoLog(E_WARNING); + $errorHandler->logUncaught(E_WARNING); $this->assertSame($callback, $errorHandler->getChainedErrorHandler()); } @@ -347,10 +237,10 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $errorHandler->expects($this->once())->method('registerShutdownFunction') ->with([$errorHandler, 'shutdownFunction']); - $errorHandler->alsoLog(E_PARSE); + $errorHandler->logUncaught(E_PARSE); // Subsequent calls should have no effect - $errorHandler->alsoLog(E_PARSE); + $errorHandler->logUncaught(E_PARSE); $this->assertAttributeNotEmpty('reservedMemory', $errorHandler); } @@ -382,11 +272,11 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase /** * @dataProvider errorHandlerProvider * - * @param int $alsoLog + * @param int $logUncaught * @param int $code * @param InvokedCount $expectLog */ - public function testHandleErrorWithLogging($alsoLog, $code, InvokedCount $expectLog) + public function testHandleErrorWithLogging($logUncaught, $code, InvokedCount $expectLog) { $logger = $this->createMock(LoggerInterface::class); $logger->expects($expectLog)->method('log') @@ -396,7 +286,7 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $errorHandler->expects($this->once())->method('errorReporting')->willReturn(E_ALL | E_STRICT); $errorHandler->setLogger($logger); - $errorHandler->alsoLog($alsoLog); + $errorHandler->logUncaught($logUncaught); $this->errorHandler->handleError($code, 'no good', 'foo.php', 42, []); } @@ -404,12 +294,12 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase /** * @dataProvider errorHandlerProvider * - * @param int $alsoLog Ignored + * @param int $logUncaught Ignored * @param int $code * @param InvokedCount $expectLog Ignored * @param boolean $expectException */ - public function testHandleErrorWithConvertError($alsoLog, $code, InvokedCount $expectLog, $expectException) + public function testHandleErrorWithConvertError($logUncaught, $code, InvokedCount $expectLog, $expectException) { $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->never())->method('log'); @@ -454,11 +344,11 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase /** * @dataProvider shutdownFunctionProvider * - * @param int $alsoLog Ignored + * @param int $logUncaught Ignored * @param int $code * @param InvokedCount $expectLog Ignored */ - public function testShutdownFunction($alsoLog, $code, InvokedCount $expectLog) + public function testShutdownFunction($logUncaught, $code, InvokedCount $expectLog) { $logger = $this->createMock(LoggerInterface::class); $logger->expects($expectLog)->method('log') @@ -477,7 +367,7 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase ->willReturn($code ? $error : null); $errorHandler->setLogger($logger); - $errorHandler->alsoLog($alsoLog); + $errorHandler->logUncaught($logUncaught); $this->assertAttributeNotEmpty('reservedMemory', $errorHandler); |