Browse Source

Everything's a mess; committing just so I can go back if I need to.

2.1.0
Dustin Wilson 2 years ago
parent
commit
845d048896
  1. 49
      lib/Catcher.php
  2. 84
      lib/Catcher/HTMLHandler.php
  3. 102
      lib/Catcher/Handler.php
  4. 205
      lib/Catcher/PlainTextHandler.php
  5. 72
      lib/Catcher/ThrowableHandler.php

49
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']);
}
}

84
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(
'<!doctype html><html><head><title>%s%s</title></head><body></body></html>',
(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;

102
lib/Catcher/Handler.php

@ -0,0 +1,102 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace Mensbeam\Framework\Catcher;
abstract class Handler {
public const CONTENT_TYPE = null;
public const CONTINUE = 1;
public const BREAK = 2;
public const EXIT = 4;
public const OUTPUT = 16;
public const OUTPUT_NOW = 32;
public const SILENT = 64;
protected ThrowableController $controller;
protected array $data;
/** The handler's result (bitmask) */
protected int $outputCode;
/**
* If true the handler will break the handler stack and won't continue onto the
* next handler regardless
*/
protected static bool $_forceBreak = false;
/** If true the handler will continue onto the next handler regardless */
protected static bool $_forceContinue = false;
/** If true the handler will force an exit */
protected static bool $_forceExit = false;
/**
* If true the handler will output as soon as possible; however, if silent
* is true the handler will output nothing
*/
protected static bool $_forceOutputNow = false;
/** If true the handler will be silent and won't output */
protected static bool $_silent = false;
protected function __construct(ThrowableController $controller, array $data = []) {
$this->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;
}

205
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();

72
lib/Catcher/ThrowableHandler.php

@ -1,72 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace Mensbeam\Framework\Catcher;
abstract class ThrowableHandler {
public const CONTENT_TYPE = null;
/** If true the handler will output data; if false it will be silent */
protected bool $_output = true;
/**
* If true the handler will pass on through to the next handler even if it
* successfully handles the throwable; if false it will prevent execution of the
* next handler if it successfully handles the throwable
*/
protected bool $_passthrough = false;
/**
* The result of the handler's processing of the throwable; most of the time this
* will be a string, but in some cases like the HTMLHandler it's a
* DOMDocumentFragment so it may be further manipulated
*/
protected mixed $_result = null;
public function __construct(array $config = []) {
foreach ($config as $key => $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);
}
}
Loading…
Cancel
Save