summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArnold Daniels <arnold@jasny.net>2016-10-28 02:02:29 +0200
committerArnold Daniels <arnold@jasny.net>2016-10-28 02:02:29 +0200
commitf5eb8869ae3f60b0e44cba7e273fd2ed5c63e1a8 (patch)
tree744749d657e031d9dbef085784bc293312bba55d
parent1ef376a930f400a8173e3cb8bfb08923622bcfe5 (diff)
downloaderror-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.md100
-rw-r--r--composer.json2
-rw-r--r--src/ErrorHandler.php352
-rw-r--r--src/ErrorHandler/ErrorCodes.php84
-rw-r--r--src/ErrorHandler/Logging.php104
-rw-r--r--src/ErrorHandler/Middleware.php76
-rw-r--r--tests/ErrorHandler/MiddlewareTest.php143
-rw-r--r--tests/ErrorHandlerTest.php184
8 files changed, 638 insertions, 407 deletions
diff --git a/README.md b/README.md
index db15966..515d07d 100644
--- a/README.md
+++ b/README.md
@@ -5,21 +5,7 @@ Jasny Error Handler
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jasny/error-handler/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jasny/error-handler/?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/jasny/error-handler/badges/coverage.png?b=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);