Browse Source

Okay things are working a bit better now

2.1.0
Dustin Wilson 2 years ago
parent
commit
fa4b7a9878
  1. 7
      composer.json
  2. 488
      composer.lock
  3. 47
      lib/Catcher.php
  4. 213
      lib/Catcher/HTMLHandler.php
  5. 141
      lib/Catcher/Handler.php
  6. 23
      lib/Catcher/HandlerOutput.php
  7. 198
      lib/Catcher/PlainTextHandler.php
  8. 4
      lib/Catcher/ThrowableController.php
  9. 2
      lib/Error.php

7
composer.json

@ -5,8 +5,8 @@
"license": "MIT",
"autoload": {
"psr-4": {
"Mensbeam\\Framework\\": "lib/",
"Mensbeam\\Framework\\Test\\": "test/"
"MensBeam\\Framework\\": "lib/",
"MensBeam\\Framework\\Test\\": "test/"
}
},
"authors": [
@ -21,5 +21,8 @@
},
"suggest": {
"ext-dom": "For HTMLHandler"
},
"require-dev": {
"mensbeam/html-dom": "^1.0"
}
}

488
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8998cce6e05b3ccc6aca7c736416f570",
"content-hash": "e8aa70aef9ba115203828355980ff484",
"packages": [
{
"name": "psr/log",
@ -57,7 +57,491 @@
"time": "2021-07-14T16:46:02+00:00"
}
],
"packages-dev": [],
"packages-dev": [
{
"name": "mensbeam/framework",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/mensbeam/Framework.git",
"reference": "af4b8e72d50c01cf78d3d1b12b5a65f714f9d73b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mensbeam/Framework/zipball/af4b8e72d50c01cf78d3d1b12b5a65f714f9d73b",
"reference": "af4b8e72d50c01cf78d3d1b12b5a65f714f9d73b",
"shasum": ""
},
"require": {
"php": ">=7.4"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"MensBeam\\Framework\\": [
"lib/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dustin Wilson",
"email": "dustin@dustinwilson.com",
"homepage": "https://dustinwilson.com/"
}
],
"description": "Common classes and traits used in many MensBeam projects",
"support": {
"issues": "https://github.com/mensbeam/Framework/issues",
"source": "https://github.com/mensbeam/Framework/tree/1.0.5"
},
"time": "2022-01-05T16:09:09+00:00"
},
{
"name": "mensbeam/html-dom",
"version": "1.0.8",
"source": {
"type": "git",
"url": "https://github.com/mensbeam/HTML-DOM.git",
"reference": "8a9f0e18b2bc14dc6e451690be9ff0aeeb428496"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mensbeam/HTML-DOM/zipball/8a9f0e18b2bc14dc6e451690be9ff0aeeb428496",
"reference": "8a9f0e18b2bc14dc6e451690be9ff0aeeb428496",
"shasum": ""
},
"require": {
"ext-dom": "*",
"mensbeam/framework": "^1.0.4",
"mensbeam/html-parser": "^1.2.1",
"php": ">=8.0.2",
"symfony/css-selector": "^5.3"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.3",
"mikey179/vfsstream": "^1.6",
"nikic/php-parser": "^4.13"
},
"type": "library",
"autoload": {
"psr-4": {
"MensBeam\\HTML\\DOM\\": [
"lib/",
"lib/HTMLElement"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dustin Wilson",
"email": "dustin@dustinwilson.com",
"homepage": "https://dustinwilson.com/"
},
{
"name": "J. King",
"email": "jking@jkingweb.ca",
"homepage": "https://jkingweb.ca/"
}
],
"description": "Modern DOM library written in PHP for HTML documents",
"support": {
"issues": "https://github.com/mensbeam/HTML-DOM/issues",
"source": "https://github.com/mensbeam/HTML-DOM/tree/1.0.8"
},
"time": "2022-02-12T15:46:39+00:00"
},
{
"name": "mensbeam/html-parser",
"version": "1.2.5",
"source": {
"type": "git",
"url": "https://github.com/mensbeam/HTML-Parser.git",
"reference": "02ec5df3fe985b3b6635c0cfd37df10ccd023e4c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mensbeam/HTML-Parser/zipball/02ec5df3fe985b3b6635c0cfd37df10ccd023e4c",
"reference": "02ec5df3fe985b3b6635c0cfd37df10ccd023e4c",
"shasum": ""
},
"require": {
"ext-dom": "*",
"mensbeam/intl": ">=0.9.1",
"mensbeam/mimesniff": ">=0.2.0",
"php": ">=7.1"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.3"
},
"suggest": {
"ext-ctype": "Improved performance"
},
"type": "library",
"autoload": {
"files": [
"lib/Parser/ctype.php"
],
"psr-4": {
"MensBeam\\HTML\\": [
"lib/"
]
},
"classmap": [
"lib/Parser/Token.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dustin Wilson",
"email": "dustin@dustinwilson.com",
"homepage": "https://dustinwilson.com/"
},
{
"name": "J. King",
"email": "jking@jkingweb.ca",
"homepage": "https://jkingweb.ca/"
}
],
"description": "Parser and serializer for modern HTML documents",
"keywords": [
"HTML5",
"WHATWG",
"dom",
"html",
"parser",
"parsing"
],
"support": {
"issues": "https://github.com/mensbeam/HTML-Parser/issues",
"source": "https://github.com/mensbeam/HTML-Parser/tree/1.2.5"
},
"time": "2022-02-15T20:47:49+00:00"
},
{
"name": "mensbeam/intl",
"version": "0.9.1",
"source": {
"type": "git",
"url": "https://github.com/mensbeam/intl.git",
"reference": "07d26e3f45c3a3167eb6389572419d3bda7ff5e1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mensbeam/intl/zipball/07d26e3f45c3a3167eb6389572419d3bda7ff5e1",
"reference": "07d26e3f45c3a3167eb6389572419d3bda7ff5e1",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"bamarni/composer-bin-plugin": "*",
"ext-intl": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"MensBeam\\Intl\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "J. King",
"email": "jking@jkingweb.ca",
"homepage": "https://jkingweb.ca/"
}
],
"description": "A set of dependency-free basic internationalization tools",
"keywords": [
"WHATWG",
"charset",
"encoding",
"internationalization",
"intl",
"unicode",
"utf-8",
"utf8"
],
"support": {
"issues": "https://github.com/mensbeam/intl/issues",
"source": "https://github.com/mensbeam/intl/tree/0.9.1"
},
"time": "2021-10-24T14:37:46+00:00"
},
{
"name": "mensbeam/mimesniff",
"version": "0.2.1",
"source": {
"type": "git",
"url": "https://github.com/mensbeam/mime.git",
"reference": "c19be2496ab1e27fbf9c3483c2a9faa2781796cd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mensbeam/mime/zipball/c19be2496ab1e27fbf9c3483c2a9faa2781796cd",
"reference": "c19be2496ab1e27fbf9c3483c2a9faa2781796cd",
"shasum": ""
},
"require": {
"php": ">=7.1",
"psr/http-message": "^1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.3",
"ext-intl": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"MensBeam\\Mime\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "J. King",
"email": "jking@jkingweb.ca",
"homepage": "https://jkingweb.ca/"
}
],
"description": "An implementation of the WHATWG MIME Sniffing specification",
"keywords": [
"WHATWG",
"mime",
"mimesniff"
],
"support": {
"issues": "https://github.com/mensbeam/mime/issues",
"source": "https://github.com/mensbeam/mime/tree/0.2.1"
},
"time": "2021-03-07T03:58:00+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/master"
},
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "symfony/css-selector",
"version": "v5.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "c1681789f059ab756001052164726ae88512ae3d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/c1681789f059ab756001052164726ae88512ae3d",
"reference": "c1681789f059ab756001052164726ae88512ae3d",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.16"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v5.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-06-27T16:58:25+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-05-10T07:21:04+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],

47
lib/Catcher.php

@ -6,8 +6,8 @@
*/
declare(strict_types=1);
namespace Mensbeam\Framework;
use Mensbeam\Framework\Catcher\{
namespace MensBeam\Framework;
use MensBeam\Framework\Catcher\{
ThrowableController,
Handler
};
@ -20,18 +20,14 @@ class Catcher {
* @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(string ...$handlerClasses) {
$this->handlerClasses = $handlerClasses;
$this->results = new Map();
public function __construct(Handler ...$handlers) {
$this->handlers = $handlers;
set_error_handler([ $this, 'handleError' ]);
set_exception_handler([ $this, 'handleThrowable' ]);
@ -69,30 +65,29 @@ class Catcher {
*/
public function handleThrowable(\Throwable $throwable): void {
$controller = new ThrowableController($throwable);
foreach ($this->handlerClasses as $h) {
$handler = $h::create($controller);
$code = $handler->getOutputCode();
if ($code & Handler::OUTPUT_NOW) {
$handler->output();
} else {
$this->handlers[] = $handler;
foreach ($this->handlers as $h) {
$output = $h->handle($controller);
if ($output->outputCode & Handler::OUTPUT_NOW) {
$h->dispatch();
}
if ($code & Handler::CONTINUE) {
$controlCode = $output->controlCode;
if ($controlCode !== 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 (
$this->isShuttingDown ||
$throwable instanceof \Exception ||
in_array($throwable->getCode(), [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR ])
($throwable instanceof Error && in_array($throwable->getCode(), [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR ])) ||
$throwable instanceof \Error
) {
foreach ($this->handlers as $h) {
$h->dispatch();
}
exit($throwable->getCode());
} elseif ($controlCode === Handler::EXIT) {
exit($throwable->getCode());
}
}
@ -104,8 +99,10 @@ class Catcher {
*/
public function handleShutdown() {
$this->isShuttingDown = true;
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']);
if ($error = error_get_last()) {
if (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']);
}
}
}

213
lib/Catcher/HTMLHandler.php

@ -6,81 +6,145 @@
*/
declare(strict_types=1);
namespace Mensbeam\Framework\Catcher;
namespace MensBeam\Framework\Catcher;
class HTMLHandler extends Handler {
public const CONTENT_TYPE = 'text/html';
/** The number of backtrace frames in which to print arguments; defaults to 5 */
protected static int $_backtraceArgFrameLimit = 5;
/** If true the handler will output backtraces; defaults to false */
protected static bool $_outputBacktrace = false;
/** If true the handler will output previous throwables; defaults to true */
protected static bool $_outputPrevious = true;
protected ?\DOMDocument $_document = null;
protected static array $bullshit = [];
/** If true the handler will output times to the output; defaults to true */
protected static bool $_outputTime = true;
protected bool $_outputTime = true;
/** The PHP-standard date format which to use for times printed to output */
protected static string $_timeFormat = 'H:i:s';
protected string $_timeFormat = 'H:i:s';
public static function handle(\Throwable $throwable, ThrowableController $controller): bool {
$document = new \DOMDocument();
$frag = $document->createDocumentFragment();
public function __construct(array $options = []) {
parent::__construct($options);
if ($this->_document === null) {
$this->_document = new \DOMDocument();
$this->_document->loadHTML(<<<HTML
<!DOCTYPE html>
<html>
<head><title>HTTP {$this->_httpCode}</title></head>
<body></body>
</html>
HTML);
}
}
protected function buildThrowable(ThrowableController $controller): \DOMElement {
$throwable = $controller->getThrowable();
$p = $this->_document->createElement('p');
$class = $throwable::class;
if ($throwable instanceof \Error) {
$type = $controller->getErrorType();
if ($type !== null) {
$b = $this->_document->createElement('b');
$b->appendChild($this->_document->createTextNode($type));
$p->appendChild($b);
$p->appendChild($this->_document->createTextNode(' ('));
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($throwable::class));
$p->appendChild($code);
$p->appendChild($this->_document->createTextNode(')'));
} else {
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($throwable::class));
$p->appendChild($code);
}
}
$p->appendChild($this->_document->createTextNode(': '));
$i = $this->_document->createElement('i');
$i->appendChild($this->_document->createTextNode($throwable->getMessage()));
$p->appendChild($i);
$p->appendChild($this->_document->createTextNode(' in file '));
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($throwable->getFile()));
$p->appendChild($code);
$p->appendChild($this->_document->createTextNode(' on line ' . $throwable->getLine()));
return $p;
}
protected function dispatchCallback(): void {
$body = $this->_document->getElementsByTagName('body')[0];
foreach ($this->outputBuffer as $o) {
if ($o->outputCode & self::SILENT) {
continue;
}
$body->appendChild($o->output);
}
if (self::$_outputTime && self::$_timeFormat !== '') {
$p = $document->createElement('p');
$time = $document->createElement('time');
$time->appendChild($document->createTextNode((new \DateTime())->format(self::$_timeFormat)));
$output = $this->_document->saveHTML();
if (\PHP_SAPI === 'CLI') {
fprintf(\STDERR, "$output\n");
} else {
echo $output;
}
}
protected function handleCallback(ThrowableController $controller): HandlerOutput {
$frag = $this->_document->createDocumentFragment();
if ($this->_outputTime && $this->_timeFormat !== '') {
$p = $this->_document->createElement('p');
$time = $this->_document->createElement('time');
$time->appendChild($this->_document->createTextNode((new \DateTime())->format($this->_timeFormat)));
$p->appendChild($time);
$frag->appendChild($p);
}
$frag->appendChild(self::$buildThrowable($document, $throwable, $controller));
if (self::$_outputPrevious) {
$prev = $throwable->getPrevious();
$frag->appendChild($this->buildThrowable($controller));
if ($this->_outputPrevious) {
$prevController = $controller->getPrevious();
while ($prev) {
$p = $document->createElement('p');
$small = $document->createElement('small');
$small->appendChild($document->createTextNode('Caused by ↴'));
while ($prevController) {
$p = $this->_document->createElement('p');
$small = $this->_document->createElement('small');
$small->appendChild($this->_document->createTextNode('Caused by ↴'));
$p->appendChild($small);
$frag->appendChild($p);
$frag->appendChild(self::$buildThrowable($document, $prev, $prevController));
$prev = $prev->getPrevious();
$frag->appendChild($this->buildThrowable($prevController));
$prevController = $prevController->getPrevious();
}
}
if (self::$_outputBacktrace) {
if ($this->_outputBacktrace) {
$frames = $controller->getFrames();
$p = $document->createElement('p');
$p->appendChild($document->createTextNode('Stack trace:'));
$p = $this->_document->createElement('p');
$p->appendChild($this->_document->createTextNode('Stack trace:'));
$frag->appendChild($p);
if (count($frames) > 0) {
$ol = $document->createElement('ol');
$ol = $this->_document->createElement('ol');
$p->appendChild($ol);
$num = 1;
foreach ($frames as $frame) {
$li = $document->createElement('li');
$args = (!empty($frame['args']) && self::$_backtraceArgFrameLimit >= $num);
$t = ($args) ? $document->createElement('p') : $li;
$li = $this->_document->createElement('li');
$args = (!empty($frame['args']) && $this->_backtraceArgFrameLimit >= $num);
$t = ($args) ? $this->_document->createElement('p') : $li;
if (!empty($frame['error'])) {
$b = $document->createElement('b');
$b->appendChild($document->createTextNode($frame['error']));
$b = $this->_document->createElement('b');
$b->appendChild($this->_document->createTextNode($frame['error']));
$t->appendChild($b);
$t->appendChild($document->createTextNode(' ('));
$code = $document->createElement('code');
$code->appendChild($document->createTextNode($frame['class']));
$t->appendChild($this->_document->createTextNode(' ('));
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($frame['class']));
$t->appendChild($code);
$t->appendChild($document->createTextNode(')'));
$t->appendChild($this->_document->createTextNode(')'));
} elseif (!empty($frame['class'])) {
$code = $document->createElement('code');
$code->appendChild($document->createTextNode($frame['class']));
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($frame['class']));
$t->appendChild($code);
}
@ -90,22 +154,22 @@ class HTMLHandler extends Handler {
if ($class) {
$code->firstChild->textContent .= "::{$function}()";
} else {
$code = $document->createElement('code');
$code->appendChild($document->createTextNode("{$function}()"));
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode("{$function}()"));
$t->appendChild($code);
}
}
$t->appendChild($document->createTextNode(' '));
$i = $document->createElement('i');
$i->appendChild($document->createTextNode($frame['file']));
$t->appendChild($this->_document->createTextNode(' '));
$i = $this->_document->createElement('i');
$i->appendChild($this->_document->createTextNode($frame['file']));
$t->appendChild($i);
$t->appendChild($document->createTextNode(":{$frame['line']}"));
$t->appendChild($this->_document->createTextNode(":{$frame['line']}"));
if ($args) {
$li->appendChild($t);
$pre = $document->createElement('pre');
$pre->appendChild($document->createTextNode(var_export($frame['args'], true)));
$pre = $this->_document->createElement('pre');
$pre->appendChild($this->_document->createTextNode(var_export($frame['args'], true)));
$li->appendChild($pre);
}
@ -114,57 +178,6 @@ class HTMLHandler extends Handler {
}
}
$this->result = $frag;
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']} " : '',
'500 Internal Server Error'
));
$document->getElementsByTagName('body')[0]->appendChild($document->importNode($frag, true));
self::$sendContentTypeHeader();
http_response_code(500);
echo $document->saveHTML();
return (!self::$_passthrough);
}
return false;
}
protected static 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;
return new HandlerOutput($this->getControlCode(), $this->getOutputCode(), $frag);
}
}

141
lib/Catcher/Handler.php

@ -6,46 +6,87 @@
*/
declare(strict_types=1);
namespace Mensbeam\Framework\Catcher;
namespace MensBeam\Framework\Catcher;
abstract class Handler {
public const CONTENT_TYPE = null;
// Control constants
public const CONTINUE = 1;
public const BREAK = 2;
public const EXIT = 4;
// Output constants
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
* Array of HandlerOutputs the handler creates
*
* @var HandlerOutput[]
*/
protected array $outputBuffer = [];
/**
* Array of option property names; used when overloading
*
* @var string[]
*/
protected static bool $_forceBreak = false;
protected array $optionNames;
/** The number of backtrace frames in which to print arguments; defaults to 5 */
protected int $_backtraceArgFrameLimit = 5;
/**
* The character encoding used for errors; only used if headers weren't sent before
* an error occurred
*/
protected string $_charset = 'UTF-8';
/** If true the handler will continue onto the next handler regardless */
protected static bool $_forceContinue = false;
protected bool $_forceContinue = false;
/** If true the handler will force an exit */
protected static bool $_forceExit = false;
protected 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;
protected bool $_forceOutputNow = false;
/** The HTTP code to be sent */
protected int $_httpCode = 500;
/** 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 be silent and won't output */
protected static bool $_silent = false;
protected bool $_silent = false;
public function __construct(array $options = []) {
foreach ($options as $key => $value) {
$key = "_$key";
if ($key === '_httpCode' && is_int($value) && ($value < 400 || $value >= 600)) {
throw new \InvalidArgumentException('Option "httpCode" can only be an integer between 400 and 599');
}
$this->$key = $value;
}
$properties = (new \ReflectionClass($this))->getProperties(\ReflectionProperty::IS_PROTECTED);
$this->optionNames = [];
foreach ($properties as $p) {
$name = $p->getName();
if ($name[0] === '_') {
$this->optionNames[] = $name;
}
}
}
protected function __construct(ThrowableController $controller, array $data = []) {
/*protected function __construct(ThrowableController $controller, array $data = []) {
$this->controller = $controller;
$this->data = $data;
@ -73,30 +114,80 @@ abstract class Handler {
$this->outputCode |= ($this->outputCode === self::SILENT) ? self::CONTINUE : self::BREAK;
return;
}
}*/
public function dispatch(): void {
if (count($this->outputBuffer) === 0) {
return;
}
public static function config(array $config = []) {
foreach ($config as $key => $value) {
$key = "_$key";
self::$$key = $value;
// Send the headers if possible and necessary
if (isset($_SERVER['REQUEST_URI'])) {
if (!headers_sent()) {
header_remove('location');
header(sprintf('Content-type: %s; charset=%s', static::CONTENT_TYPE, $this->_charset));
}
http_response_code($this->_httpCode);
}
return __CLASS__;
$this->dispatchCallback();
$this->outputBuffer = [];
}
public function handle(ThrowableController $controller): HandlerOutput {
$output = $this->handleCallback($controller);
$this->outputBuffer[] = $output;
return $output;
}
abstract public static function create(ThrowableController $controller): self;
abstract protected function dispatchCallback(): void;
public function getOutputCode(): int {
return $this->outputCode;
/*protected function createOutput(mixed $output): HandlerOutput {
return new HandlerOutput($this->getControlCode(), $this->getOutputCode(), $output);
}*/
protected function getControlCode(): int {
$code = self::BREAK;
if ($this->_forceExit) {
$code = self::EXIT;
} elseif ($this->_forceContinue) {
$code = self::CONTINUE;
}
return $code;
}
public function getThrowable(): \Throwable {
return $this->throwable;
protected function getOutputCode(): int {
$code = self::OUTPUT;
if ($this->_silent) {
$code = self::SILENT;
if ($this->_forceOutputNow) {
$code |= self::OUTPUT_NOW;
}
} elseif ($this->_forceOutputNow) {
$code = self::OUTPUT_NOW;
}
return $code;
}
abstract protected function handleCallback(ThrowableController $controller): HandlerOutput;
/*public function __get(string $name): mixed {
$name = "_$name";
if (in_array($name, $this->optionNames)) {
return $this->$name;
}
}
abstract public function output(): void;
public function __set(string $name, mixed $value): void {
$name = "_$name";
if (in_array($name, $this->optionNames)) {
$this->$name = $value;
}
}*/
}

23
lib/Catcher/HandlerOutput.php

@ -0,0 +1,23 @@
<?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 HandlerOutput {
public readonly int $controlCode;
public readonly mixed $output;
public readonly int $outputCode;
public function __construct(int $controlCode, int $outputCode, mixed $output) {
$this->controlCode = $controlCode;
$this->outputCode = $outputCode;
$this->output = $output;
}
}

198
lib/Catcher/PlainTextHandler.php

@ -6,151 +6,68 @@
*/
declare(strict_types=1);
namespace Mensbeam\Framework\Catcher;
use \Psr\Log\{
LoggerAwareInterface,
LoggerInterface
};
namespace MensBeam\Framework\Catcher;
use \Psr\Log\LoggerInterface;
class PlainTextHandler extends Handler implements LoggerAwareInterface {
class PlainTextHandler extends Handler {
public const CONTENT_TYPE = 'text/plain';
/** The number of backtrace frames in which to print arguments; defaults to 5 */
protected static int $_backtraceArgFrameLimit = 5;
/** The PSR-3 compatible logger in which to log to; defaults to null (no logging) */
protected static ?LoggerInterface $_logger = null;
/** If true the handler will output backtraces; defaults to false */
protected static bool $_outputBacktrace = false;
/** If true the handler will output previous throwables; defaults to 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 static bool $_outputTime = true;
/** The PHP-standard date format which to use for times printed to output */
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);
}
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);
}
protected ?LoggerInterface $_logger = null;
/** 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 timestamps in output */
protected string $_timeFormat = '[H:i:s]';
if (!Catcher::isHTTPRequest()) {
fprintf(\STDERR, "$message\n");
} else {
echo "$message\n";
}
}
public function getOutputCode(): int {
if ($this->outputCode !== null) {
return $this->outputCode;
}
$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;
}
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;
protected function dispatchCallback(): void {
foreach ($this->outputBuffer as $o) {
if ($o->outputCode & self::SILENT) {
continue;
}
} elseif ($throwable instanceof \Exception) {
if ($throwable instanceof \PharException || $throwable instanceof \RuntimeException) {
self::$_logger->alert($message);
if (\PHP_SAPI === 'CLI') {
fprintf(\STDERR, "{$o->output}\n");
} else {
echo "{$o->output}\n";
}
} else {
self::$_logger->critical($message);
}
}
protected function prependTimestamps(string $message): string {
$time = (new \DateTime())->format(self::$_timeFormat) . ' ';
$timeStrlen = strlen($time);
$message = preg_replace('/^/m', str_repeat(' ', $timeStrlen), $message);
return preg_replace('/^ {' . $timeStrlen . '}/', $time, $message);
}
protected static function serialize(ThrowableController $controller): string {
$message = self::$serializeThrowable($controller);
if (self::$_outputPrevious) {
$prev = $throwable->getPrevious();
protected function handleCallback(ThrowableController $controller): HandlerOutput {
$output = $this->serializeThrowable($controller);
if ($this->_outputPrevious) {
$prevController = $controller->getPrevious();
while ($prev) {
$message .= sprintf("\n\nCaused by ↴\n%s", self::$serializeThrowable($prev, $prevController));
$prev = $prev->getPrevious();
while ($prevController) {
$output .= sprintf("\n\nCaused by ↴\n%s", $this->serializeThrowable($prevController));
$prevController = $prevController->getPrevious();
}
}
if (self::$_outputBacktrace) {
if ($this->_outputBacktrace) {
$frames = $controller->getFrames();
$message .= "\nStack trace:";
$output .= "\nStack trace:";
$num = 1;
$maxDigits = strlen((string)count($frames));
foreach ($frames as $frame) {
$class = (!empty($frame['error'])) ? "{$frame['error']} ({$frame['class']})" : $frame['class'] ?? '';
$function = $frame['function'] ?? '';
$args = '';
if (!empty($frame['args']) && self::$_backtraceArgFrameLimit >= $num) {
$args = "\n" . preg_replace('/^/m', str_repeat(' ', strlen((string)$num) + 2) . '| ', var_export($frame['args'], true));
if (!empty($frame['args']) && $this->_backtraceArgFrameLimit >= $num) {
$args = "\n" . preg_replace('/^/m', str_repeat(' ', $maxDigits) . '| ', var_export($frame['args'], true));
}
$template = "\n%3d. %s";
$template = "\n%{$maxDigits}d. %s";
if ($class && $function) {
$template .= '::';
}
$template .= ($function) ? '%s()' : '%s';
$template .= ' %s:%d%s';
$message .= sprintf(
$output .= sprintf(
"$template\n",
$num++,
$class,
@ -159,17 +76,68 @@ class PlainTextHandler extends Handler implements LoggerAwareInterface {
$frame['line'],
$args
);
die($output);
}
}
return $message;
// The logger will handle timestamps itself.
if ($this->_logger !== null) {
$this->log($controller->getThrowable(), $message);
}
if (!$this->_silent && $this->_outputTime && $this->_timeFormat !== '') {
$time = (new \DateTime())->format($this->_timeFormat) . ' ';
$timeStrlen = strlen($time);
$output = preg_replace('/^/m', str_repeat(' ', $timeStrlen), $output);
$output = preg_replace('/^ {' . $timeStrlen . '}/', $time, $output);
}
$outputCode = $this->getOutputCode();
return new HandlerOutput($this->getControlCode(), ($outputCode === self::OUTPUT && \PHP_SAPI === 'CLI') ? self::OUTPUT_NOW : $outputCode, $output);
}
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;
default: $this->_logger->critical($message);
}
} elseif ($throwable instanceof \Exception && ($throwable instanceof \PharException || $throwable instanceof \RuntimeException)) {
$this->_logger->alert($message);
} else {
$this->_logger->critical($message);
}
}
protected static function serializeThrowable(ThrowableController $controller): string {
protected function serializeThrowable(ThrowableController $controller): string {
$throwable = $controller->getThrowable();
$class = $throwable::class;
if ($throwable instanceof \Error) {
if ($throwable instanceof Error) {
$type = $controller->getErrorType();
$type = ($throwable instanceof Error) ? $controller->getErrorType() : null;
$class = ($type !== null) ? "$type (" . $throwable::class . ")" : $throwable::class;
}

4
lib/Catcher/ThrowableController.php

@ -6,7 +6,7 @@
*/
declare(strict_types=1);
namespace Mensbeam\Framework\Catcher;
namespace MensBeam\Framework\Catcher;
class ThrowableController {
@ -31,7 +31,7 @@ class ThrowableController {
return $this->errorType;
}
if (!$this->throwable instanceof \Error) {
if (!$this->throwable instanceof Error) {
$this->errorType = null;
return null;
}

2
lib/Error.php

@ -6,7 +6,7 @@
*/
declare(strict_types=1);
namespace Mensbeam\Framework;
namespace MensBeam\Framework;
class Error extends \Error {
public function __construct(string $message = '', int $code = 0, ?string $file = null, ?int $line = line, ?\Throwable $previous = null) {

Loading…
Cancel
Save