diff --git a/composer.json b/composer.json index c865354..805fdbd 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ ], "require": { "php": ">=8.1", - "psr/log": "^3.0" + "psr/log": "^3.0", + "mikey179/vfsstream": "^1.6" }, "require-dev": { "phpunit/phpunit": "^10", diff --git a/composer.lock b/composer.lock index 38f7c76..d43d52e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,59 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "264b81873cadf8449e400b7902f0ef98", + "content-hash": "8098556523bac180121bef4ca07349e6", "packages": [ + { + "name": "mikey179/vfsstream", + "version": "v1.6.11", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2022-02-23T02:02:42+00:00" + }, { "name": "psr/log", "version": "3.0.0", @@ -189,16 +240,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.4", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -239,9 +290,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2023-03-05T19:49:14+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "phake/phake", @@ -426,16 +477,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.0.2", + "version": "10.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "20800e84296ea4732f9a125e08ce86b4004ae3e4" + "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/20800e84296ea4732f9a125e08ce86b4004ae3e4", - "reference": "20800e84296ea4732f9a125e08ce86b4004ae3e4", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/db1497ec8dd382e82c962f7abbe0320e4882ee4e", + "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e", "shasum": "" }, "require": { @@ -454,7 +505,7 @@ "theseer/tokenizer": "^1.2.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -463,7 +514,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.0-dev" + "dev-main": "10.1-dev" } }, "autoload": { @@ -491,7 +542,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.0.2" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.2" }, "funding": [ { @@ -499,20 +551,20 @@ "type": "github" } ], - "time": "2023-03-06T13:00:19+00:00" + "time": "2023-05-22T09:04:27+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "4.0.1", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "fd9329ab3368f59fe1fe808a189c51086bd4b6bd" + "reference": "5647d65443818959172645e7ed999217360654b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/fd9329ab3368f59fe1fe808a189c51086bd4b6bd", - "reference": "fd9329ab3368f59fe1fe808a189c51086bd4b6bd", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/5647d65443818959172645e7ed999217360654b6", + "reference": "5647d65443818959172645e7ed999217360654b6", "shasum": "" }, "require": { @@ -551,7 +603,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.1" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.2" }, "funding": [ { @@ -559,7 +612,7 @@ "type": "github" } ], - "time": "2023-02-10T16:53:14+00:00" + "time": "2023-05-07T09:13:23+00:00" }, { "name": "phpunit/php-invoker", @@ -744,16 +797,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.0.19", + "version": "10.2.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "20c23e85c86e5c06d63538ba464e8054f4744e62" + "reference": "35c8cac1734ede2ae354a6644f7088356ff5b08e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/20c23e85c86e5c06d63538ba464e8054f4744e62", - "reference": "20c23e85c86e5c06d63538ba464e8054f4744e62", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/35c8cac1734ede2ae354a6644f7088356ff5b08e", + "reference": "35c8cac1734ede2ae354a6644f7088356ff5b08e", "shasum": "" }, "require": { @@ -767,7 +820,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.0", + "phpunit/php-code-coverage": "^10.1.1", "phpunit/php-file-iterator": "^4.0", "phpunit/php-invoker": "^4.0", "phpunit/php-text-template": "^3.0", @@ -793,7 +846,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.0-dev" + "dev-main": "10.2-dev" } }, "autoload": { @@ -825,7 +878,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.19" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.2.3" }, "funding": [ { @@ -841,7 +894,7 @@ "type": "tidelift" } ], - "time": "2023-03-27T11:46:33+00:00" + "time": "2023-06-30T06:17:38+00:00" }, { "name": "sebastian/cli-parser", @@ -1145,16 +1198,16 @@ }, { "name": "sebastian/diff", - "version": "5.0.1", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "aae9a0a43bff37bd5d8d0311426c87bf36153f02" + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/aae9a0a43bff37bd5d8d0311426c87bf36153f02", - "reference": "aae9a0a43bff37bd5d8d0311426c87bf36153f02", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b", "shasum": "" }, "require": { @@ -1200,7 +1253,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/diff/tree/5.0.3" }, "funding": [ { @@ -1208,20 +1261,20 @@ "type": "github" } ], - "time": "2023-03-23T05:12:41+00:00" + "time": "2023-05-01T07:48:21+00:00" }, { "name": "sebastian/environment", - "version": "6.0.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "b6f3694c6386c7959915a0037652e0c40f6f69cc" + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b6f3694c6386c7959915a0037652e0c40f6f69cc", - "reference": "b6f3694c6386c7959915a0037652e0c40f6f69cc", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", "shasum": "" }, "require": { @@ -1263,7 +1316,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/6.0.0" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" }, "funding": [ { @@ -1271,7 +1325,7 @@ "type": "github" } ], - "time": "2023-02-03T07:03:04+00:00" + "time": "2023-04-11T05:39:26+00:00" }, { "name": "sebastian/exporter", diff --git a/lib/Catcher/Handler.php b/lib/Catcher/Handler.php index 4ca0245..5ae65eb 100644 --- a/lib/Catcher/Handler.php +++ b/lib/Catcher/Handler.php @@ -159,7 +159,7 @@ abstract class Handler { set_exception_handler($exceptionHandler); // If the current exception handler happens to not be Catcher use PHP's handler - // instead; this shouldn't happen but is here just in case + // instead; this shouldn't happen in normal operation but is here just in case if (!is_array($exceptionHandler) || !$exceptionHandler[0] instanceof Catcher) { return false; } @@ -168,18 +168,23 @@ abstract class Handler { // varExporter to prevent infinite looping of error handlers $catcher = $exceptionHandler[0]; $handlers = $catcher->getHandlers(); - $silent = false; + $handlersCount = count($handlers); + $silentCount = 0; foreach ($handlers as $h) { $h->setOption('logger', null); $h->setOption('varExporter', null); - $silent = (!$silent) ? $h->getOption('silent') : $silent; + + if ($h->getOption('silent')) { + $silentCount++; + } } // If all of the handlers are silent then use PHP's handler instead; this is // because a valid use for Catcher is to have it be silent but instead have the // logger print the errors to stderr/stdout; if there is an error in the logger // then it wouldn't print. - if ($silent) { + if ($silentCount === $handlersCount) { + // TODO: Output an error here to state that Catcher failed? return false; } @@ -310,13 +315,7 @@ abstract class Handler { protected function varExporter(mixed $value): string|bool { $exporter = $this->_varExporter; set_error_handler([ $this, 'handleError' ]); - if ($exporter === null) { - $value = print_r($value, true); - restore_error_handler(); - return $value; - } - - $value = $exporter($value); + $value = ($exporter === null) ? print_r($value, true) : $exporter($value); restore_error_handler(); return $value; } diff --git a/lib/Catcher/PlainTextHandler.php b/lib/Catcher/PlainTextHandler.php index 6d2158c..2b7bddb 100644 --- a/lib/Catcher/PlainTextHandler.php +++ b/lib/Catcher/PlainTextHandler.php @@ -49,7 +49,7 @@ class PlainTextHandler extends Handler { $outputThrowable['line'] ); - if (!empty($outputThrowable['previous'])) { + if (isset($outputThrowable['previous']) && $outputThrowable['previous'] instanceof \Throwable) { if ($previous) { $output .= ' '; } @@ -57,24 +57,19 @@ class PlainTextHandler extends Handler { } if (!$previous) { - if (isset($outputThrowable['frames']) && count($outputThrowable['frames']) > 0) { + if (isset($outputThrowable['frames']) && is_array($outputThrowable['frames']) && count($outputThrowable['frames']) > 0) { $output .= \PHP_EOL . 'Stack trace:' . \PHP_EOL; $maxDigits = strlen((string)count($outputThrowable['frames'])); $indent = str_repeat(' ', $maxDigits); foreach ($outputThrowable['frames'] as $key => $frame) { - $method = null; - if (!empty($frame['class'])) { - if (!empty($frame['errorType'])) { + $method = $frame['class'] ?? "{$frame['function']}()" ?? null; + if (isset($frame['class']) && $method === $frame['class']) { + if (isset($frame['errorType'])) { $method = "{$frame['errorType']} ({$frame['class']})"; - } else { - $method = $frame['class']; - if (!empty($frame['function'])) { - $ref = new \ReflectionMethod($frame['class'], $frame['function']); - $method .= (($ref->isStatic()) ? '::' : '->') . $frame['function']; - } + } elseif (isset($frame['function'])) { + $ref = new \ReflectionMethod($frame['class'], $frame['function']); + $method .= (($ref->isStatic()) ? '::' : '->') . $frame['function'] . '()'; } - } elseif (!empty($frame['function'])) { - $method = $frame['function']; } $output .= sprintf("%{$maxDigits}d. %s %s:%d" . \PHP_EOL, @@ -84,8 +79,7 @@ class PlainTextHandler extends Handler { $frame['line'] ); - if (!empty($frame['args']) && $this->_backtraceArgFrameLimit > $key) { - $varExporter = $this->_varExporter; + if (isset($frame['args']) && $this->_backtraceArgFrameLimit > $key) { $output .= preg_replace('/^/m', "$indent| ", $this->varExporter($frame['args'])) . \PHP_EOL; } } @@ -100,7 +94,7 @@ class PlainTextHandler extends Handler { if (!empty($outputThrowable['time'])) { $timestamp = $outputThrowable['time']->format($this->_timeFormat) . ' '; - $output = ltrim(preg_replace('/^/m', str_repeat(' ', strlen($timestamp)), "$timestamp$output")); + $output = ltrim(preg_replace('/^(?=\h*\S)/m', str_repeat(' ', strlen($timestamp)), "$timestamp$output")); } } diff --git a/lib/Catcher/ThrowableController.php b/lib/Catcher/ThrowableController.php index 0341ede..447cb1c 100644 --- a/lib/Catcher/ThrowableController.php +++ b/lib/Catcher/ThrowableController.php @@ -151,25 +151,25 @@ class ThrowableController { } // Add a frame for the throwable to the beginning of the array - $f = [ - 'file' => $this->throwable->getFile() ?: '[UNKNOWN]', - 'line' => (int)$this->throwable->getLine(), - 'class' => $this->throwable::class, - 'args' => [ - $this->throwable->getMessage() - ] - ]; - - // Add the error code and type if it is an Error. - if ($this->throwable instanceof \Error) { - $errorType = $this->getErrorType(); - if ($errorType !== null) { - $f['code'] = $this->throwable->getCode(); - $f['errorType'] = $errorType; - } - } - - array_unshift($frames, $f); + // $f = [ + // 'file' => $this->throwable->getFile() ?: '[UNKNOWN]', + // 'line' => (int)$this->throwable->getLine(), + // 'class' => $this->throwable::class, + // 'args' => [ + // $this->throwable->getMessage() + // ] + // ]; + + // // Add the error code and type if it is an Error. + // if ($this->throwable instanceof \Error) { + // $errorType = $this->getErrorType(); + // if ($errorType !== null) { + // $f['code'] = $this->throwable->getCode(); + // $f['errorType'] = $errorType; + // } + // } + + // array_unshift($frames, $f); // Go through previous throwables and merge in their frames if ($prev = $this->getPrevious()) { diff --git a/tests/cases/TestHandler.php b/tests/cases/TestHandler.php index 848e9e9..c1b03f4 100644 --- a/tests/cases/TestHandler.php +++ b/tests/cases/TestHandler.php @@ -13,7 +13,8 @@ use MensBeam\Catcher\{ RangeException, ThrowableController }; -use Psr\Log\LoggerInterface, +use MensBeam\Catcher, + Psr\Log\LoggerInterface, Phake; @@ -78,7 +79,7 @@ class TestHandler extends ErrorHandlingTestCase { ob_start(); $h(); $o = ob_get_clean(); - $this->assertNotNull($o); + $this->assertNotEmpty($o); $o = json_decode($o, true); $this->assertSame(\Exception::class, $o['class']); $this->assertSame(__FILE__, $o['file']); @@ -86,6 +87,31 @@ class TestHandler extends ErrorHandlingTestCase { $this->assertSame('Ook!', $o['message']); } + /** @dataProvider provideSupplementalErrorHandlingTests */ + public function testSupplementalErrorHandling(\Closure $closure, bool $useCatcher, bool $silent): void { + if (!$silent) { + $this->handler->setOption('print', true); + $this->handler->setOption('printJSON', false); + $this->handler->setOption('outputToStderr', false); + } else { + $this->handler->setOption('silent', true); + } + + if ($useCatcher) { + $c = new Catcher($this->handler); + } + + ob_start(); + $closure($this->handler); + $o = ob_get_clean(); + $this->assertNotEmpty($o); + + if ($useCatcher) { + $c->unregister(); + unset($c); + } + } + public function testFatalError(): void { $this->expectException(RangeException::class); @@ -191,4 +217,31 @@ class TestHandler extends ErrorHandlingTestCase { yield $o; } } + + public static function provideSupplementalErrorHandlingTests(): iterable { + $iterable = [ + // Test with a logger that errors without a Catcher + [ function (Handler $h): void { + $h->setOption('logger', new FailLogger()); + $h->handle(new ThrowableController(new \Exception('Ook!'))); + $h(); + }, false, false ], + // Test with a logger that errors with a Catcher + [ function (Handler $h): void { + $h->setOption('logger', new FailLogger()); + $h->handle(new ThrowableController(new \Exception('Ook!'))); + $h(); + }, true, false ], + // Test with a logger that errors with a Catcher but silent + [ function (Handler $h): void { + $h->setOption('logger', new FailLogger()); + $h->handle(new ThrowableController(new \Exception('Ook!'))); + $h(); + }, true, true ] + ]; + + foreach ($iterable as $i) { + yield $i; + } + } } \ No newline at end of file diff --git a/tests/lib/FailLogger.php b/tests/lib/FailLogger.php new file mode 100644 index 0000000..f0ef3b9 --- /dev/null +++ b/tests/lib/FailLogger.php @@ -0,0 +1,23 @@ +at($v); + file_put_contents($d->url(), $message); + } +} diff --git a/tests/lib/TestingHandler.php b/tests/lib/TestingHandler.php index 512aaaa..b9a8849 100644 --- a/tests/lib/TestingHandler.php +++ b/tests/lib/TestingHandler.php @@ -16,6 +16,8 @@ class TestingHandler extends Handler { protected ?string $_name = null; // Could just use silent option instead, but we need to test Handler::SILENT protected bool $_print = false; + // When printing output, print it as JSON + protected bool $_printJSON = true; protected function handleCallback(array $output): array { @@ -25,10 +27,6 @@ class TestingHandler extends Handler { protected function invokeCallback(): void { foreach ($this->outputBuffer as $o) { - if (($o['code'] & self::OUTPUT) === 0) { - continue; - } - if ($o['code'] & self::LOG) { $this->log($o['controller']->getThrowable(), json_encode([ 'class' => $o['class'], @@ -39,10 +37,14 @@ class TestingHandler extends Handler { ])); } + if (($o['code'] & self::OUTPUT) === 0) { + continue; + } + $o = $this->cleanOutputThrowable($o); if ($this->_print) { - $this->print(json_encode($o, \JSON_THROW_ON_ERROR)); + $this->print(($this->_printJSON) ? json_encode($o, \JSON_THROW_ON_ERROR) : var_dump($o) ?? 'FAIL'); } $this->output[] = $o;