Dustin Wilson
6 months ago
4 changed files with 177 additions and 14 deletions
@ -0,0 +1,68 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher; |
|||
|
|||
|
|||
class JSONHandler extends Handler { |
|||
public const CONTENT_TYPE = 'application/json'; |
|||
|
|||
/** If true the handler will pretty print JSON output; defaults to true */ |
|||
protected bool $_prettyPrint = true; |
|||
|
|||
|
|||
protected function handleCallback(array $output): array { |
|||
$output['code'] = (\PHP_SAPI === 'cli') ? $output['code'] | self::NOW : $output['code']; |
|||
return $output; |
|||
} |
|||
|
|||
protected function invokeCallback(): void { |
|||
foreach ($this->outputBuffer as $o) { |
|||
if (($o['code'] & self::OUTPUT) === 0) { |
|||
if ($o['code'] & self::LOG) { |
|||
$this->serializeOutputThrowable($o); |
|||
} |
|||
|
|||
continue; |
|||
} |
|||
|
|||
$this->print($this->serializeOutputThrowable($o)); |
|||
} |
|||
} |
|||
|
|||
|
|||
protected function serializeOutputThrowable(array $outputThrowable, bool $previous = false): array|string { |
|||
$controller = $outputThrowable['controller']; |
|||
$output = $this->cleanOutputThrowable($outputThrowable); |
|||
$jsonFlags = \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE; |
|||
if ($this->_prettyPrint) { |
|||
$jsonFlags |= \JSON_PRETTY_PRINT; |
|||
} |
|||
|
|||
if (isset($outputThrowable['previous'])) { |
|||
$output['previous'] = $this->serializeOutputThrowable($outputThrowable['previous'], true); |
|||
} |
|||
|
|||
if (!$previous) { |
|||
if (isset($outputThrowable['frames']) && is_array($outputThrowable['frames']) && count($outputThrowable['frames']) > 0) { |
|||
$output['frames'] = $outputThrowable['frames']; |
|||
} |
|||
|
|||
// The log message shouldn't have the timestamp added to it. |
|||
if ($outputThrowable['code'] & self::LOG) { |
|||
$o = $output; |
|||
unset($o['time']); |
|||
$this->log($controller->getThrowable(), json_encode($o, $jsonFlags)); |
|||
} |
|||
|
|||
$output = json_encode($output, $jsonFlags); |
|||
} |
|||
|
|||
return $output; |
|||
} |
|||
} |
@ -0,0 +1,108 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
use MensBeam\Catcher\{ |
|||
Error, |
|||
Handler, |
|||
JSONHandler, |
|||
ThrowableController |
|||
}; |
|||
use Psr\Log\LoggerInterface, |
|||
Phake; |
|||
|
|||
|
|||
/** @covers \MensBeam\Catcher\JSONHandler */ |
|||
class TestJSONHandler extends \PHPUnit\Framework\TestCase { |
|||
protected ?Handler $handler = null; |
|||
|
|||
|
|||
public function setUp(): void { |
|||
parent::setUp(); |
|||
|
|||
$this->handler = new JSONHandler([ |
|||
'outputBacktrace' => true, |
|||
'silent' => true |
|||
]); |
|||
} |
|||
|
|||
///** @dataProvider provideHandlingTests */ |
|||
/*public function testHandling(\Throwable $throwable, int $expectedCode, array $options = []): void { |
|||
foreach ($options as $k => $v) { |
|||
$this->handler->setOption($k, $v); |
|||
} |
|||
|
|||
$o = $this->handler->handle(new ThrowableController($throwable)); |
|||
$this->assertSame($throwable::class, $o['controller']->getThrowable()::class); |
|||
$this->assertEquals($expectedCode, $o['code']); |
|||
}*/ |
|||
|
|||
/** @dataProvider provideInvocationTests */ |
|||
public function testInvocation(\Throwable $throwable, bool $silent, bool $log, ?string $logMethodName, int $line): void { |
|||
$this->handler->setOption('outputToStderr', false); |
|||
|
|||
if (!$silent) { |
|||
$this->handler->setOption('silent', false); |
|||
} |
|||
if ($log) { |
|||
$l = Phake::mock(LoggerInterface::class); |
|||
$this->handler->setOption('logger', $l); |
|||
} |
|||
|
|||
$o = $this->handler->handle(new ThrowableController($throwable)); |
|||
$c = $o['class'] ?? null; |
|||
|
|||
$h = $this->handler; |
|||
ob_start(); |
|||
$h(); |
|||
$u = ob_get_clean(); |
|||
|
|||
if (!$silent) { |
|||
$u = json_decode($u, true); |
|||
$this->assertEquals($c, $u['class']); |
|||
$this->assertEquals(__FILE__, $u['file']); |
|||
$this->assertEquals($line, $u['line']); |
|||
} else { |
|||
$this->assertSame('', $u); |
|||
} |
|||
|
|||
if ($log) { |
|||
Phake::verify($l, Phake::times(1))->$logMethodName; |
|||
} |
|||
} |
|||
|
|||
|
|||
public static function provideHandlingTests(): iterable { |
|||
$options = [ |
|||
[ new \Exception('Ook!'), Handler::BUBBLES | Handler::EXIT, [ 'forceExit' => true ] ], |
|||
[ new \Error('Ook!'), Handler::BUBBLES ], |
|||
[ new Error('Ook!', \E_ERROR, '/dev/null', 42, new \Error('Eek!')), Handler::BUBBLES | Handler::NOW, [ 'forceOutputNow' => true ] ], |
|||
[ new \Exception('Ook!'), Handler::BUBBLES, [ 'logger' => Phake::mock(LoggerInterface::class), 'logWhenSilent' => false ] ], |
|||
[ new \Error('Ook!'), Handler::BUBBLES | Handler::LOG, [ 'forceOutputNow' => true, 'logger' => Phake::mock(LoggerInterface::class) ] ] |
|||
]; |
|||
|
|||
foreach ($options as $o) { |
|||
$o[1] |= Handler::NOW; |
|||
yield $o; |
|||
} |
|||
} |
|||
|
|||
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' ] |
|||
]; |
|||
|
|||
$l = count($options); |
|||
foreach ($options as $k => $o) { |
|||
yield [ ...$o, __LINE__ - 4 - $l + $k ]; |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue