diff options
Diffstat (limited to 'src')
-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 |
4 files changed, 402 insertions, 214 deletions
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; + } +} |