Browse Source

Meh. Things are a bit of a mess.

2.1.0
Dustin Wilson 2 years ago
parent
commit
cad6c2b977
  1. 3
      composer.json
  2. 11
      lib/Catcher.php
  3. 216
      lib/Catcher/HTMLHandler.php
  4. 2
      lib/Catcher/PlainTextHandler.php
  5. 18
      lib/Catcher/ThrowableHandler.php

3
composer.json

@ -18,5 +18,8 @@
"require": {
"php": ">=8.1",
"psr/log": "^3.0"
},
"suggest": {
"ext-dom": "For HTMLHandler"
}
}

11
lib/Catcher.php

@ -80,7 +80,12 @@ class Catcher {
}
}
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 ])
) {
exit($throwable->getCode());
}
}
@ -98,4 +103,8 @@ class Catcher {
$this->handleError($error['type'], $error['message'], $error['file'], $error['line']);
}
}
public static function isHTTPRequest() {
return (\PHP_SAPI !== 'cli' && isset($_SERVER['REQUEST_URI']) && isset($_SERVER['REQUEST_METHOD']));
}
}

216
lib/Catcher/HTMLHandler.php

@ -0,0 +1,216 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace Mensbeam\Framework\Catcher;
class HTMLHandler extends ThrowableHandler {
public const CONTENT_TYPE = 'text/html';
/** The number of backtrace frames in which to print arguments; defaults to 5 */
protected int $_backtraceArgFrameLimit = 5;
/** If true the handler will output backtraces; defaults to false */
protected bool $_outputBacktrace = false;
/** If true the handler will output previous throwables; defaults to true */
protected bool $_outputPrevious = true;
/** If true the handler will output times to the output; defaults to true */
protected bool $_outputTime = true;
/** The PHP-standard date format which to use for times printed to output */
protected 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 {
$document = new \DOMDocument();
$frag = $document->createDocumentFragment();
if ($this->_outputTime && $this->_timeFormat !== '') {
$p = $document->createElement('p');
$time = $document->createElement('time');
$time->appendChild($document->createTextNode((new \DateTime())->format($this->_timeFormat)));
$p->appendChild($time);
$frag->appendChild($p);
}
$frag->appendChild($this->buildThrowable($document, $throwable, $controller));
if ($this->_outputPrevious) {
$prev = $throwable->getPrevious();
$prevController = $controller->getPrevious();
while ($prev) {
$p = $document->createElement('p');
$small = $document->createElement('small');
$small->appendChild($document->createTextNode('Caused by ↴'));
$p->appendChild($small);
$frag->appendChild($p);
$frag->appendChild($this->buildThrowable($document, $prev, $prevController));
$prev = $prev->getPrevious();
$prevController = $prevController->getPrevious();
}
}
if ($this->_outputBacktrace) {
$frames = $controller->getFrames();
$p = $document->createElement('p');
$p->appendChild($document->createTextNode('Stack trace:'));
$frag->appendChild($p);
if (count($frames) > 0) {
$ol = $document->createElement('ol');
$p->appendChild($ol);
$num = 1;
foreach ($frames as $frame) {
$li = $document->createElement('li');
$args = (!empty($frame['args']) && $this->_backtraceArgFrameLimit >= $num);
$t = ($args) ? $document->createElement('p') : $li;
if (!empty($frame['error'])) {
$b = $document->createElement('b');
$b->appendChild($document->createTextNode($frame['error']));
$t->appendChild($b);
$t->appendChild($document->createTextNode(' ('));
$code = $document->createElement('code');
$code->appendChild($document->createTextNode($frame['class']));
$t->appendChild($code);
$t->appendChild($document->createTextNode(')'));
} elseif (!empty($frame['class'])) {
$code = $document->createElement('code');
$code->appendChild($document->createTextNode($frame['class']));
$t->appendChild($code);
}
$class = $frame['class'] ?? '';
$function = $frame['function'] ?? '';
if ($function) {
if ($class) {
$code->firstChild->textContent .= "::{$function}()";
} else {
$code = $document->createElement('code');
$code->appendChild($document->createTextNode("{$function}()"));
$t->appendChild($code);
}
}
$t->appendChild($document->createTextNode(' '));
$i = $document->createElement('i');
$i->appendChild($document->createTextNode($frame['file']));
$t->appendChild($i);
$t->appendChild($document->createTextNode(":{$frame['line']}"));
if ($args) {
$li->appendChild($t);
$pre = $document->createElement('pre');
$pre->appendChild($document->createTextNode(var_export($frame['args'], true)));
$li->appendChild($pre);
}
$ol->appendChild($li);
}
}
}
$this->_result = $frag;
if (\PHP_SAPI !== 'cli' && $this->_output) {
$document->loadHTML(sprintf(
'<!doctype html><html><head><title>%s%s</title></head><body></body></html>',
(isset($_SERVER['protocol'])) ? "{$_SERVER['protocol']} " : '',
'500 Internal Server Error'
));
$document->getElementsByTagName('body')[0]->appendChild($document->importNode($frag, true));
$this->sendContentTypeHeader();
http_response_code(500);
echo $document->saveHTML();
return (!$this->_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 {
$p = $document->createElement('p');
$class = $throwable::class;
if ($throwable instanceof \Error) {
$type = $controller->getErrorType();
if ($type !== null) {
$b = $document->createElement('b');
$b->appendChild($document->createTextNode($type));
$p->appendChild($b);
$p->appendChild($document->createTextNode(' ('));
$code = $document->createElement('code');
$code->appendChild($document->createTextNode($throwable::class));
$p->appendChild($code);
$p->appendChild($document->createTextNode(')'));
} else {
$code = $document->createElement('code');
$code->appendChild($document->createTextNode($throwable::class));
$p->appendChild($code);
}
}
$p->appendChild($document->createTextNode(': '));
$i = $document->createElement('i');
$i->appendChild($document->createTextNode($throwable->getMessage()));
$p->appendChild($i);
$p->appendChild($document->createTextNode(' in file '));
$code = $document->createElement('code');
$code->appendChild($document->createTextNode($throwable->getFile()));
$p->appendChild($code);
$p->appendChild($document->createTextNode(' on line ' . $throwable->getLine()));
return $p;
}
}

2
lib/Catcher/PlainTextHandler.php

@ -14,7 +14,7 @@ use \Psr\Log\{
class PlainTextHandler extends ThrowableHandler implements LoggerAwareInterface {
protected static ?string $contentType = 'text/plain';
public const CONTENT_TYPE = 'text/plain';
/** The number of backtrace frames in which to print arguments; defaults to 5 */
protected int $_backtraceArgFrameLimit = 5;

18
lib/Catcher/ThrowableHandler.php

@ -10,7 +10,7 @@ namespace Mensbeam\Framework\Catcher;
abstract class ThrowableHandler {
protected static ?string $contentType = null;
public const CONTENT_TYPE = null;
/** If true the handler will output data; if false it will be silent */
protected bool $_output = true;
@ -20,6 +20,12 @@ abstract class ThrowableHandler {
* 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;
@ -32,11 +38,7 @@ abstract class ThrowableHandler {
}
public function getContentType(): ?string {
return static::$contentType;
}
public function getOutput(): bool {
return $this->_output;
@ -46,6 +48,10 @@ abstract class ThrowableHandler {
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 {
@ -61,6 +67,6 @@ abstract class ThrowableHandler {
return;
}
header('Content-Type: ' . static::$contentType);
header('Content-Type: ' . static::CONTENT_TYPE);
}
}
Loading…
Cancel
Save