diff --git a/README.md b/README.md index 54b446f..040b84a 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ abstract class Handler { protected bool $_forceExit = false; protected bool $_forceOutputNow = false; protected int $_httpCode = 500; + protected ?array $_ignore = null; protected ?LoggerInterface $_logger = null; protected bool $_logWhenSilent = true; protected bool $_outputBacktrace = false; @@ -241,6 +242,7 @@ _forceBreak_: When set this will force the stack loop to break after the handler _forceExit_: When set this will force an exit after all handlers have been run. Defaults to _false_. _forceOutputNow_: When set this will force output of the handler immediately. Defaults to _false_. _httpCode_: The HTTP code to be sent; possible values are 200, 400-599. Defaults to _500_. +_ignore_: An array of class strings or error codes to ignore. Defaults to _null_ (no ignoring). _logger_: The PSR-3 compatible logger in which to log to. Defaults to _null_ (no logging). _logWhenSilent_: When set to true the handler will still send logs when silent. Defaults to _true_. _outputBacktrace_: When true will output a stack trace. Defaults to _false_. diff --git a/lib/Catcher/Handler.php b/lib/Catcher/Handler.php index 3256879..3f7c871 100644 --- a/lib/Catcher/Handler.php +++ b/lib/Catcher/Handler.php @@ -50,6 +50,11 @@ abstract class Handler { protected bool $_forceOutputNow = false; /** The HTTP code to be sent; possible values: 200, 400-599 */ protected int $_httpCode = 500; + /** + * An array of class strings or error codes to ignore + * @var int[]|string[] + */ + protected array $_ignore = []; /** The PSR-3 compatible logger in which to log to; defaults to null (no logging) */ protected ?LoggerInterface $_logger = null; /** When set to true the handler will still send logs when silent */ @@ -117,30 +122,45 @@ abstract class Handler { } public function handle(ThrowableController $controller): array { - $output = $this->buildOutputArray($controller); - - if ($this->_outputBacktrace) { - $output['frames'] = $controller->getFrames(argFrameLimit: $this->_backtraceArgFrameLimit); - } - if ($this->_outputTime && $this->_timeFormat !== '') { - $output['time'] = new \DateTimeImmutable(); + $ignore = false; + if (count($this->_ignore) > 0) { + $throwable = $controller->getThrowable(); + foreach ($this->_ignore as $i) { + if (($throwable instanceof Error && is_int($i) && $throwable->getCode() === $i) || (is_string($i) && $throwable instanceof $i)) { + $ignore = true; + break; + } + } } $code = 0; if ($this->_bubbles) { $code = self::BUBBLES; } - if ($this->_forceExit) { - $code |= self::EXIT; - } - if ($this->_logger !== null && (!$this->_silent || ($this->_silent && $this->_logWhenSilent))) { - $code |= self::LOG; + if (!$ignore) { + if ($this->_forceExit) { + $code |= self::EXIT; + } + if ($this->_logger !== null && (!$this->_silent || ($this->_silent && $this->_logWhenSilent))) { + $code |= self::LOG; + } + if ($this->_forceOutputNow) { + $code |= self::NOW; + } + if (!$this->_silent) { + $code |= self::OUTPUT; + } + } else { + return [ 'code' => $code ]; } - if ($this->_forceOutputNow) { - $code |= self::NOW; + + $output = $this->buildOutputArray($controller); + + if ($this->_outputBacktrace) { + $output['frames'] = $controller->getFrames(argFrameLimit: $this->_backtraceArgFrameLimit); } - if (!$this->_silent) { - $code |= self::OUTPUT; + if ($this->_outputTime && $this->_timeFormat !== '') { + $output['time'] = new \DateTimeImmutable(); } $output['code'] = $code; @@ -205,20 +225,33 @@ abstract class Handler { return; } - if ( - $name === 'httpCode' && - is_int($value) && - $value !== 200 && - max(400, min($value, 418)) !== $value && - max(421, min($value, 429)) !== $value && - $value !== 431 && - $value !== 451 && - max(500, min($value, 511)) !== $value && - // Cloudflare extensions - max(520, min($value, 527)) !== $value && - $value !== 530 - ) { - throw new RangeException('Option "httpCode" can only be a valid HTTP 200, 4XX, or 5XX code'); + switch ($name) { + case 'httpCode': + if ( + is_int($value) && + $value !== 200 && + max(400, min($value, 418)) !== $value && + max(421, min($value, 429)) !== $value && + $value !== 431 && + $value !== 451 && + max(500, min($value, 511)) !== $value && + // Cloudflare extensions + max(520, min($value, 527)) !== $value && + $value !== 530 + ) { + throw new RangeException('Option "httpCode" can only be a valid HTTP 200, 4XX, or 5XX code'); + } + break; + + case 'ignore': + if (is_array($value)) { + foreach ($value as $v) { + if (!is_int($v) && !is_string($v)) { + throw new InvalidArgumentException('Option "ignore" can only be an array of integers and/or strings'); + } + } + } + break; } $name = "_$name"; @@ -254,6 +287,7 @@ abstract class Handler { protected function cleanOutputThrowable(array $outputThrowable): array { unset($outputThrowable['controller']); + unset($outputThrowable['code']); if (isset($outputThrowable['previous'])) { $outputThrowable['previous'] = $this->cleanOutputThrowable($outputThrowable['previous']); diff --git a/lib/Catcher/InvalidArgumentException.php b/lib/Catcher/InvalidArgumentException.php new file mode 100644 index 0000000..a1ea1bf --- /dev/null +++ b/lib/Catcher/InvalidArgumentException.php @@ -0,0 +1,11 @@ +handler::class, 'outputBuffer'); $r->setAccessible(true); $this->assertEquals(1, count($r->getValue($this->handler))); - $this->assertNull($this->handler->getLastOutputThrowable()); $h = $this->handler; $h(); - $this->assertEquals(0, count($r->getValue($h))); - $this->assertSame(\Exception::class, $h->getLastOutputThrowable()['class']); + $this->assertEquals(0, count($r->getValue($this->handler))); $h(); - $this->assertEquals(0, count($r->getValue($h))); - $this->assertSame(\Exception::class, $h->getLastOutputThrowable()['class']); + $this->assertEquals(0, count($r->getValue($this->handler))); } /** @dataProvider provideLogTests */ @@ -160,6 +158,12 @@ class TestHandler extends ErrorHandlingTestCase { $l = new FailLogger(); $l->error('Ook!'); } + ], + [ + InvalidArgumentException::class, + function (Handler $h): void { + $h->setOption('ignore', [ \M_PI ]); + } ] ]; @@ -260,6 +264,7 @@ class TestHandler extends ErrorHandlingTestCase { [ 'httpCode', 200 ], [ 'httpCode', 400 ], [ 'httpCode', 502 ], + [ 'ignore', [ \Exception::class, \E_USER_ERROR ] ], [ 'logger', Phake::mock(LoggerInterface::class) ], [ 'logWhenSilent', false ], [ 'outputBacktrace', true ], diff --git a/tests/cases/TestJSONHandler.php b/tests/cases/TestJSONHandler.php index 080e16a..83aecd0 100644 --- a/tests/cases/TestJSONHandler.php +++ b/tests/cases/TestJSONHandler.php @@ -17,7 +17,10 @@ use Psr\Log\LoggerInterface, Phake; -/** @covers \MensBeam\Catcher\JSONHandler */ +/** + * @covers \MensBeam\Catcher\JSONHandler + * @covers \MensBeam\Catcher\Handler + */ class TestJSONHandler extends \PHPUnit\Framework\TestCase { protected ?Handler $handler = null; @@ -32,7 +35,7 @@ class TestJSONHandler extends \PHPUnit\Framework\TestCase { } /** @dataProvider provideInvocationTests */ - public function testInvocation(\Throwable $throwable, bool $silent, bool $log, ?string $logMethodName, int $line): void { + public function testInvocation(\Throwable $throwable, bool $silent, bool $log, ?string $logMethodName, ?array $ignore, int $line): void { $this->handler->setOption('outputToStderr', false); if (!$silent) { @@ -42,6 +45,9 @@ class TestJSONHandler extends \PHPUnit\Framework\TestCase { $l = Phake::mock(LoggerInterface::class); $this->handler->setOption('logger', $l); } + if ($ignore !== null) { + $this->handler->setOption('ignore', $ignore); + } $o = $this->handler->handle(new ThrowableController($throwable)); $c = $o['class'] ?? null; @@ -51,17 +57,20 @@ class TestJSONHandler extends \PHPUnit\Framework\TestCase { $h(); $u = ob_get_clean(); - if (!$silent) { + if (!$silent && $ignore === null) { $u = json_decode($u, true); $this->assertEquals($c, $u['class']); $this->assertEquals(__FILE__, $u['file']); $this->assertEquals($line, $u['line']); } else { + if ($ignore !== null) { + $this->assertNull($h->getLastOutputThrowable()); + } $this->assertSame('', $u); } if ($log) { - Phake::verify($l, Phake::times(1))->$logMethodName; + Phake::verify($l, Phake::times((int)(count($ignore ?? []) === 0)))->$logMethodName; } } @@ -83,10 +92,14 @@ class TestJSONHandler extends \PHPUnit\Framework\TestCase { public static function provideInvocationTests(): iterable { $options = [ - [ new \Exception('Ook!'), false, true, 'critical' ], - [ new \Error('Ook!'), true, false, null ], - [ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error' ], - [ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical' ] + [ new \Exception('Ook!'), false, true, 'critical', null ], + [ new \Exception('Ook!'), false, true, 'critical', [ \Exception::class ] ], + [ new \Error('Ook!'), true, false, null, null ], + [ new \Error('Ook!'), true, false, null, [ \Error::class ] ], + [ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error', null ], + [ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error', [ \E_ERROR ] ], + [ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical', null ], + [ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical', [ \Exception::class ] ] ]; $l = count($options); diff --git a/tests/cases/TestPlainTextHandler.php b/tests/cases/TestPlainTextHandler.php index 05a4a55..c86ac8c 100644 --- a/tests/cases/TestPlainTextHandler.php +++ b/tests/cases/TestPlainTextHandler.php @@ -17,7 +17,10 @@ use Psr\Log\LoggerInterface, Phake; -/** @covers \MensBeam\Catcher\PlainTextHandler */ +/** + * @covers \MensBeam\Catcher\PlainTextHandler + * @covers \MensBeam\Catcher\Handler + */ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase { protected ?Handler $handler = null; @@ -32,7 +35,7 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase { } /** @dataProvider provideInvocationTests */ - public function testInvocation(\Throwable $throwable, bool $silent, bool $log, ?string $logMethodName, int $line): void { + public function testInvocation(\Throwable $throwable, bool $silent, bool $log, ?string $logMethodName, ?array $ignore, int $line): void { $this->handler->setOption('outputToStderr', false); if (!$silent) { @@ -42,6 +45,9 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase { $l = Phake::mock(LoggerInterface::class); $this->handler->setOption('logger', $l); } + if ($ignore !== null) { + $this->handler->setOption('ignore', $ignore); + } $o = $this->handler->handle(new ThrowableController($throwable)); @@ -56,14 +62,17 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase { $u = ob_get_clean(); $u = substr($u, 0, strpos($u, \PHP_EOL) ?: 0); - if (!$silent) { + if (!$silent && $ignore === null) { $this->assertMatchesRegularExpression(sprintf('/^\[[\d:]+\] %s: Ook\! in file %s on line %s$/', preg_quote($c, '/'), preg_quote(__FILE__, '/'), $line), $u); } else { + if ($ignore !== null) { + $this->assertNull($h->getLastOutputThrowable()); + } $this->assertSame('', $u); } if ($log) { - Phake::verify($l, Phake::times(1))->$logMethodName; + Phake::verify($l, Phake::times((int)(count($ignore ?? []) === 0)))->$logMethodName; } } @@ -85,10 +94,14 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase { public static function provideInvocationTests(): iterable { $options = [ - [ new \Exception('Ook!'), false, true, 'critical' ], - [ new \Error('Ook!'), true, false, null ], - [ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error' ], - [ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical' ] + [ new \Exception('Ook!'), false, true, 'critical', null ], + [ new \Exception('Ook!'), false, true, 'critical', [ \Exception::class ] ], + [ new \Error('Ook!'), true, false, null, null ], + [ new \Error('Ook!'), true, false, null, [ \Error::class ] ], + [ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error', null ], + [ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error', [ \E_ERROR ] ], + [ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical', null ], + [ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical', [ \Exception::class ] ] ]; $l = count($options);