diff --git a/composer.json b/composer.json index 4ab3bfc..5ae6d70 100644 --- a/composer.json +++ b/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" } } diff --git a/composer.lock b/composer.lock index 7fc2812..ba844a4 100644 --- a/composer.lock +++ b/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": [], diff --git a/lib/Catcher.php b/lib/Catcher.php index ebd85c0..fbc56be 100644 --- a/lib/Catcher.php +++ b/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']); + } } } diff --git a/lib/Catcher/HTMLHandler.php b/lib/Catcher/HTMLHandler.php index 32a9346..f20486b 100644 --- a/lib/Catcher/HTMLHandler.php +++ b/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(<< + + HTTP {$this->_httpCode} + + + 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( - '%s%s', - (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); } } \ No newline at end of file diff --git a/lib/Catcher/Handler.php b/lib/Catcher/Handler.php index 6e054d3..c667f0f 100644 --- a/lib/Catcher/Handler.php +++ b/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; + } + }*/ } \ No newline at end of file diff --git a/lib/Catcher/HandlerOutput.php b/lib/Catcher/HandlerOutput.php new file mode 100644 index 0000000..6a39c77 --- /dev/null +++ b/lib/Catcher/HandlerOutput.php @@ -0,0 +1,23 @@ +controlCode = $controlCode; + $this->outputCode = $outputCode; + $this->output = $output; + } +} \ No newline at end of file diff --git a/lib/Catcher/PlainTextHandler.php b/lib/Catcher/PlainTextHandler.php index 6ca37e0..21081c9 100644 --- a/lib/Catcher/PlainTextHandler.php +++ b/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; } diff --git a/lib/Catcher/ThrowableController.php b/lib/Catcher/ThrowableController.php index 9a1e85c..c523dc3 100644 --- a/lib/Catcher/ThrowableController.php +++ b/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; } diff --git a/lib/Error.php b/lib/Error.php index a69d753..85abb4f 100644 --- a/lib/Error.php +++ b/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) {