diff --git a/lib/Catcher.php b/lib/Catcher.php index 454d43d..ebd85c0 100644 --- a/lib/Catcher.php +++ b/lib/Catcher.php @@ -9,7 +9,7 @@ declare(strict_types=1); namespace Mensbeam\Framework; use Mensbeam\Framework\Catcher\{ ThrowableController, - ThrowableHandler + Handler }; @@ -17,32 +17,29 @@ class Catcher { /** * Array of handlers the exceptions are passed to * - * @var ThrowableHandler[] + * @var Handler[] */ protected array $handlers = []; + protected array $handlerClasses = []; /** Flag set when the shutdown handler is run */ protected bool $isShuttingDown = false; + protected Map $results; - public function __construct(ThrowableHandler ...$handlers) { - $this->handlers = $handlers; - $prev = set_error_handler([ $this, 'handleError' ]); - $prev = set_exception_handler([ $this, 'handleThrowable' ]); + + public function __construct(string ...$handlerClasses) { + $this->handlerClasses = $handlerClasses; + $this->results = new Map(); + + set_error_handler([ $this, 'handleError' ]); + set_exception_handler([ $this, 'handleThrowable' ]); register_shutdown_function([ $this, 'handleShutdown' ]); } - /** - * Gets the list of handlers - * - * @return ThrowableHandler[] - */ - public function getHandlers(): array { - return $this->handlers; - } /** * Converts regular errors into throwable Errors for easier handling; meant to be @@ -72,16 +69,26 @@ class Catcher { */ public function handleThrowable(\Throwable $throwable): void { $controller = new ThrowableController($throwable); - foreach ($this->handlers as $h) { - // If the handler returns true it means the handler handled the exception properly, - // and there's no reason to pass it off to another. - if ($h->handle($throwable, $controller)) { + foreach ($this->handlerClasses as $h) { + $handler = $h::create($controller); + $code = $handler->getOutputCode(); + + if ($code & Handler::OUTPUT_NOW) { + $handler->output(); + } else { + $this->handlers[] = $handler; + } + + if ($code & Handler::CONTINUE) { break; } } + /*if ($this->isShuttingDown || $throwable instanceof \Exception || in_array($throwable->getCode(), [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR ]) { + + }*/ + if ( - self::isHTTPRequest() || $this->isShuttingDown || $throwable instanceof \Exception || in_array($throwable->getCode(), [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR ]) @@ -97,9 +104,7 @@ class Catcher { */ public function handleShutdown() { $this->isShuttingDown = true; - - $error = error_get_last(); - if ($error && in_array($error['type'], [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_CORE_WARNING, \E_COMPILE_ERROR, \E_COMPILE_WARNING ])) { + if (error_get_last() && in_array($error['type'], [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_CORE_WARNING, \E_COMPILE_ERROR, \E_COMPILE_WARNING ])) { $this->handleError($error['type'], $error['message'], $error['file'], $error['line']); } } diff --git a/lib/Catcher/HTMLHandler.php b/lib/Catcher/HTMLHandler.php index 8c1042c..32a9346 100644 --- a/lib/Catcher/HTMLHandler.php +++ b/lib/Catcher/HTMLHandler.php @@ -9,64 +9,37 @@ declare(strict_types=1); namespace Mensbeam\Framework\Catcher; -class HTMLHandler extends ThrowableHandler { +class HTMLHandler extends Handler { public const CONTENT_TYPE = 'text/html'; /** The number of backtrace frames in which to print arguments; defaults to 5 */ - protected int $_backtraceArgFrameLimit = 5; + protected static int $_backtraceArgFrameLimit = 5; /** If true the handler will output backtraces; defaults to false */ - protected bool $_outputBacktrace = false; + protected static bool $_outputBacktrace = false; /** If true the handler will output previous throwables; defaults to true */ - protected bool $_outputPrevious = true; + protected static bool $_outputPrevious = true; /** If true the handler will output times to the output; defaults to true */ - protected bool $_outputTime = true; + protected static bool $_outputTime = true; /** The PHP-standard date format which to use for times printed to output */ - protected string $_timeFormat = 'H:i:s'; + protected static string $_timeFormat = 'H:i:s'; - public function __construct(array $config = []) { - parent::__construct($config); - } - - - - - public function getBacktraceArgFrameLimit(): int { - return $this->_getBacktraceArgFrameLimit; - } - - public function getOutputBacktrace(): bool { - return $this->_outputBacktrace; - } - - public function getOutputPrevious(): bool { - return $this->_outputPrevious; - } - - public function getOutputTime(): bool { - return $this->_outputTime; - } - - public function getTimeFormat(): bool { - return $this->_timeFormat; - } - - public function handle(\Throwable $throwable, ThrowableController $controller): bool { + public static function handle(\Throwable $throwable, ThrowableController $controller): bool { $document = new \DOMDocument(); $frag = $document->createDocumentFragment(); - if ($this->_outputTime && $this->_timeFormat !== '') { + if (self::$_outputTime && self::$_timeFormat !== '') { $p = $document->createElement('p'); $time = $document->createElement('time'); - $time->appendChild($document->createTextNode((new \DateTime())->format($this->_timeFormat))); + $time->appendChild($document->createTextNode((new \DateTime())->format(self::$_timeFormat))); $p->appendChild($time); $frag->appendChild($p); } - $frag->appendChild($this->buildThrowable($document, $throwable, $controller)); - if ($this->_outputPrevious) { + $frag->appendChild(self::$buildThrowable($document, $throwable, $controller)); + if (self::$_outputPrevious) { $prev = $throwable->getPrevious(); $prevController = $controller->getPrevious(); while ($prev) { @@ -75,13 +48,13 @@ class HTMLHandler extends ThrowableHandler { $small->appendChild($document->createTextNode('Caused by ↴')); $p->appendChild($small); $frag->appendChild($p); - $frag->appendChild($this->buildThrowable($document, $prev, $prevController)); + $frag->appendChild(self::$buildThrowable($document, $prev, $prevController)); $prev = $prev->getPrevious(); $prevController = $prevController->getPrevious(); } } - if ($this->_outputBacktrace) { + if (self::$_outputBacktrace) { $frames = $controller->getFrames(); $p = $document->createElement('p'); $p->appendChild($document->createTextNode('Stack trace:')); @@ -93,7 +66,7 @@ class HTMLHandler extends ThrowableHandler { $num = 1; foreach ($frames as $frame) { $li = $document->createElement('li'); - $args = (!empty($frame['args']) && $this->_backtraceArgFrameLimit >= $num); + $args = (!empty($frame['args']) && self::$_backtraceArgFrameLimit >= $num); $t = ($args) ? $document->createElement('p') : $li; if (!empty($frame['error'])) { @@ -141,9 +114,9 @@ class HTMLHandler extends ThrowableHandler { } } - $this->_result = $frag; + $this->result = $frag; - if (\PHP_SAPI !== 'cli' && $this->_output) { + if (\PHP_SAPI !== 'cli' && self::$_output) { $document->loadHTML(sprintf( '%s%s', (isset($_SERVER['protocol'])) ? "{$_SERVER['protocol']} " : '', @@ -151,36 +124,17 @@ class HTMLHandler extends ThrowableHandler { )); $document->getElementsByTagName('body')[0]->appendChild($document->importNode($frag, true)); - $this->sendContentTypeHeader(); + self::$sendContentTypeHeader(); http_response_code(500); echo $document->saveHTML(); - return (!$this->_passthrough); + return (!self::$_passthrough); } return false; } - public function setBacktraceArgFrameLimit(int $value): void { - $this->_getBacktraceArgFrameLimit = $value; - } - - public function setOutputBacktrace(bool $value): void { - $this->_outputBacktrace = $value; - } - - public function setOutputPrevious(bool $value): void { - $this->_outputPrevious = $value; - } - - public function setOutputTime(bool $value): void { - $this->_outputTime = $value; - } - - public function setTimeFormat(bool $value): void { - $this->_timeFormat = $value; - } - protected function buildThrowable(\DOMDocument $document, \Throwable $throwable, ThrowableController $controller): \DOMElement { + protected static function buildThrowable(\DOMDocument $document, \Throwable $throwable, ThrowableController $controller): \DOMElement { $p = $document->createElement('p'); $class = $throwable::class; diff --git a/lib/Catcher/Handler.php b/lib/Catcher/Handler.php new file mode 100644 index 0000000..6e054d3 --- /dev/null +++ b/lib/Catcher/Handler.php @@ -0,0 +1,102 @@ +controller = $controller; + $this->data = $data; + + if (!self::$_silent) { + $this->outputCode = (!self::$_forceOutputNow) ? self::OUTPUT : self::OUTPUT_NOW; + } else { + $this->outputCode = self::SILENT; + } + + if ($forceContinue) { + $this->outputCode |= self::CONTINUE; + return; + } elseif ($forceExit) { + $this->outputCode |= self::EXIT; + return; + } + + if ($this->outputCode !== self::SILENT) { + $throwable = $controller->getThrowable(); + if ($throwable instanceof \Exception || in_array($throwable->getCode(), [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR ])) { + $this->outputCode |= self::EXIT; + return; + } + } + + $this->outputCode |= ($this->outputCode === self::SILENT) ? self::CONTINUE : self::BREAK; + return; + } + + + + + public static function config(array $config = []) { + foreach ($config as $key => $value) { + $key = "_$key"; + self::$$key = $value; + } + + return __CLASS__; + } + + abstract public static function create(ThrowableController $controller): self; + + + public function getOutputCode(): int { + return $this->outputCode; + } + + public function getThrowable(): \Throwable { + return $this->throwable; + } + + abstract public function output(): void; +} \ No newline at end of file diff --git a/lib/Catcher/PlainTextHandler.php b/lib/Catcher/PlainTextHandler.php index 0eaa487..6ca37e0 100644 --- a/lib/Catcher/PlainTextHandler.php +++ b/lib/Catcher/PlainTextHandler.php @@ -13,78 +13,123 @@ use \Psr\Log\{ }; -class PlainTextHandler extends ThrowableHandler implements LoggerAwareInterface { +class PlainTextHandler extends Handler implements LoggerAwareInterface { public const CONTENT_TYPE = 'text/plain'; /** The number of backtrace frames in which to print arguments; defaults to 5 */ - protected int $_backtraceArgFrameLimit = 5; + protected static int $_backtraceArgFrameLimit = 5; /** The PSR-3 compatible logger in which to log to; defaults to null (no logging) */ - protected ?LoggerInterface $_logger = null; + protected static ?LoggerInterface $_logger = null; /** If true the handler will output backtraces; defaults to false */ - protected bool $_outputBacktrace = false; + protected static bool $_outputBacktrace = false; /** If true the handler will output previous throwables; defaults to true */ - protected bool $_outputPrevious = true; + protected static bool $_outputPrevious = true; /** * If true the handler will output times to the output. This is ignored by the * logger which should have its own timestamping methods; defaults to true */ - protected bool $_outputTime = true; + protected static bool $_outputTime = true; /** The PHP-standard date format which to use for times printed to output */ - protected string $_timeFormat = '[H:i:s]'; - + protected static string $_timeFormat = '[H:i:s]'; + public static function create(ThrowableController $controller): self { + $message = null; + if (self::$_logger !== null) { + $message = self::serialize($controller); + self::$log($controller->getThrowable(), $message); + } - public function __construct(array $config = []) { - parent::__construct($config); + return new self( + controller: $controller, + data: [ 'message' => $message ] + ); } + public function output(): void { + $message = $this->data['message'] ?? self::serialize($this->controller); + if (self::$_outputTime && self::$_timeFormat !== '') { + $time = (new \DateTime())->format(self::$_timeFormat) . ' '; + $timeStrlen = strlen($time); + $message = preg_replace('/^/m', str_repeat(' ', $timeStrlen), $message); + $message = preg_replace('/^ {' . $timeStrlen . '}/', $time, $message); + } - public function getBacktraceArgFrameLimit(): int { - return $this->_getBacktraceArgFrameLimit; + if (!Catcher::isHTTPRequest()) { + fprintf(\STDERR, "$message\n"); + } else { + echo "$message\n"; + } } - public function getLogger(): ?LoggerInterface { - return $this->_logger; - } + public function getOutputCode(): int { + if ($this->outputCode !== null) { + return $this->outputCode; + } - public function getOutputBacktrace(): bool { - return $this->_outputBacktrace; + $code = parent::getOutputCode(); + // When the sapi is CLI we want to output as soon as possible if + $this->outputCode = ($code & self::OUTPUT === 0 && \PHP_SAPI === 'CLI') ? $code &~ self::OUTPUT | self::OUTPUT_NOW : $code; + return $this->outputCode; } - public function getOutputPrevious(): bool { - return $this->_outputPrevious; - } - public function getOutputTime(): bool { - return $this->_outputTime; + protected static function log(\Throwable $throwable, string $message): void { + if ($throwable instanceof \Error) { + switch ($throwable->getCode()) { + case \E_NOTICE: + case \E_USER_NOTICE: + case \E_STRICT: + self::$_logger->notice($message); + break; + case \E_WARNING: + case \E_COMPILE_WARNING: + case \E_USER_WARNING: + case \E_DEPRECATED: + case \E_USER_DEPRECATED: + self::$_logger->warning($message); + break; + case \E_RECOVERABLE_ERROR: + self::$_logger->error($message); + break; + case \E_PARSE: + case \E_CORE_ERROR: + case \E_COMPILE_ERROR: + self::$_logger->alert($message); + break; + } + } elseif ($throwable instanceof \Exception) { + if ($throwable instanceof \PharException || $throwable instanceof \RuntimeException) { + self::$_logger->alert($message); + } + } else { + self::$_logger->critical($message); + } } - public function getTimeFormat(): bool { - return $this->_timeFormat; - } + protected function prependTimestamps(string $message): string { + $time = (new \DateTime())->format(self::$_timeFormat) . ' '; + $timeStrlen = strlen($time); - public function handle(\Throwable $throwable, ThrowableController $controller): bool { - // If this can't output and there's no logger to log to then there's nothing to do - // here. Continue on to the next handler. - if (!$this->_output && $this->_logger === null) { - return false; - } + $message = preg_replace('/^/m', str_repeat(' ', $timeStrlen), $message); + return preg_replace('/^ {' . $timeStrlen . '}/', $time, $message); + } - $message = $this->serializeThrowable($throwable, $controller); - if ($this->_outputPrevious) { + protected static function serialize(ThrowableController $controller): string { + $message = self::$serializeThrowable($controller); + if (self::$_outputPrevious) { $prev = $throwable->getPrevious(); $prevController = $controller->getPrevious(); while ($prev) { - $message .= sprintf("\n\nCaused by ↴\n%s", $this->serializeThrowable($prev, $prevController)); + $message .= sprintf("\n\nCaused by ↴\n%s", self::$serializeThrowable($prev, $prevController)); $prev = $prev->getPrevious(); $prevController = $prevController->getPrevious(); } } - if ($this->_outputBacktrace) { + if (self::$_outputBacktrace) { $frames = $controller->getFrames(); $message .= "\nStack trace:"; @@ -94,7 +139,7 @@ class PlainTextHandler extends ThrowableHandler implements LoggerAwareInterface $function = $frame['function'] ?? ''; $args = ''; - if (!empty($frame['args']) && $this->_backtraceArgFrameLimit >= $num) { + if (!empty($frame['args']) && self::$_backtraceArgFrameLimit >= $num) { $args = "\n" . preg_replace('/^/m', str_repeat(' ', strlen((string)$num) + 2) . '| ', var_export($frame['args'], true)); } @@ -117,93 +162,11 @@ class PlainTextHandler extends ThrowableHandler implements LoggerAwareInterface } } - if ($this->_logger !== null) { - $this->log($throwable, $message); - } - - if ($this->_output) { - // Logger handles its own timestamps - if ($this->_outputTime && $this->_timeFormat !== '') { - $time = (new \DateTime())->format($this->_timeFormat) . ' '; - $timeStrlen = strlen($time); - - $message = preg_replace('/^/m', str_repeat(' ', $timeStrlen), $message); - $message = preg_replace('/^ {' . $timeStrlen . '}/', $time, $message); - } - - if (\PHP_SAPI === 'cli') { - fprintf(\STDERR, "$message\n"); - } else { - $this->sendContentTypeHeader(); - http_response_code(500); - echo "$message\n"; - } - - return (!$this->_passthrough); - } - - return false; - } - - public function setBacktraceArgFrameLimit(int $value): void { - $this->_getBacktraceArgFrameLimit = $value; - } - - public function setLogger(?LoggerInterface $value): void { - $this->_logger = $value; - } - - public function setOutputBacktrace(bool $value): void { - $this->_outputBacktrace = $value; - } - - public function setOutputPrevious(bool $value): void { - $this->_outputPrevious = $value; - } - - public function setOutputTime(bool $value): void { - $this->_outputTime = $value; - } - - public function setTimeFormat(bool $value): void { - $this->_timeFormat = $value; - } - - - protected function log(\Throwable $throwable, string $message): void { - if ($throwable instanceof \Error) { - switch ($throwable->getCode()) { - case \E_NOTICE: - case \E_USER_NOTICE: - case \E_STRICT: - $this->_logger->notice($message); - break; - case \E_WARNING: - case \E_COMPILE_WARNING: - case \E_USER_WARNING: - case \E_DEPRECATED: - case \E_USER_DEPRECATED: - $this->_logger->warning($message); - break; - case \E_RECOVERABLE_ERROR: - $this->_logger->error($message); - break; - case \E_PARSE: - case \E_CORE_ERROR: - case \E_COMPILE_ERROR: - $this->_logger->alert($message); - break; - } - } elseif ($throwable instanceof \Exception) { - if ($throwable instanceof \PharException || $throwable instanceof \RuntimeException) { - $this->_logger->alert($message); - } - } - - $this->_logger->critical($message); + return $message; } - protected function serializeThrowable(\Throwable $throwable, ThrowableController $controller): string { + protected static function serializeThrowable(ThrowableController $controller): string { + $throwable = $controller->getThrowable(); $class = $throwable::class; if ($throwable instanceof \Error) { $type = $controller->getErrorType(); diff --git a/lib/Catcher/ThrowableHandler.php b/lib/Catcher/ThrowableHandler.php deleted file mode 100644 index 1b39429..0000000 --- a/lib/Catcher/ThrowableHandler.php +++ /dev/null @@ -1,72 +0,0 @@ - $value) { - $key = "_$key"; - $this->$key = $value; - } - } - - - - - public function getOutput(): bool { - return $this->_output; - } - - public function getPassthrough(): bool { - return $this->_passthrough; - } - - public function getResult(): mixed { - return $this->_result; - } - - abstract public function handle(\Throwable $throwable, ThrowableController $controller): bool; - - public function setOutput(bool $value): void { - $this->_output = $value; - } - - public function setPassthrough(bool $value): void { - $this->_passthrough = $value; - } - - protected function sendContentTypeHeader(): void { - if (!isset($_SERVER['REQUEST_URI']) || headers_sent()) { - return; - } - - header('Content-Type: ' . static::CONTENT_TYPE); - } -} \ No newline at end of file