diff --git a/src/Exceptions/src/Attribute/NonReportable.php b/src/Exceptions/src/Attribute/NonReportable.php new file mode 100644 index 000000000..9514189f6 --- /dev/null +++ b/src/Exceptions/src/Attribute/NonReportable.php @@ -0,0 +1,10 @@ + */ protected array $reporters = []; protected mixed $output = null; + protected array $nonReportableExceptions = [ + ClientException::class, + AuthorizationException::class, + ValidationException::class, + ]; public function __construct() { @@ -64,6 +73,10 @@ public function canRender(string $format): bool public function report(\Throwable $exception): void { + if ($this->shouldNotReport($exception)) { + return; + } + foreach ($this->reporters as $reporter) { try { if ($reporter instanceof ExceptionReporterInterface) { @@ -106,6 +119,14 @@ public function addRenderer(ExceptionRendererInterface $renderer): void \array_unshift($this->renderers, $renderer); } + /** + * @param class-string<\Throwable> $exception + */ + public function dontReport(string $exception): void + { + $this->nonReportableExceptions[] = $exception; + } + /** * @param ExceptionReporterInterface|Closure(\Throwable):void $reporter */ @@ -160,4 +181,17 @@ protected function bootBasicHandlers(): void { $this->addRenderer(new PlainRenderer()); } + + protected function shouldNotReport(\Throwable $exception): bool + { + foreach ($this->nonReportableExceptions as $nonReportableException) { + if ($exception instanceof $nonReportableException) { + return true; + } + } + + $attribute = (new \ReflectionClass($exception))->getAttributes(NonReportable::class)[0] ?? null; + + return $attribute !== null; + } } diff --git a/src/Exceptions/tests/ExceptionHandlerTest.php b/src/Exceptions/tests/ExceptionHandlerTest.php index d5b63885f..23236b5de 100644 --- a/src/Exceptions/tests/ExceptionHandlerTest.php +++ b/src/Exceptions/tests/ExceptionHandlerTest.php @@ -5,11 +5,19 @@ namespace Spiral\Tests\Exceptions; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RuntimeException; use Spiral\Exceptions\ExceptionHandler; use Spiral\Exceptions\ExceptionRendererInterface; use Spiral\Exceptions\ExceptionReporterInterface; +use Spiral\Filters\Exception\AuthorizationException; +use Spiral\Filters\Exception\ValidationException; +use Spiral\Http\Exception\ClientException; +use Spiral\Http\Exception\ClientException\ForbiddenException; +use Spiral\Http\Exception\ClientException\NotFoundException; +use Spiral\Http\Exception\ClientException\UnauthorizedException; +use Spiral\Tests\Exceptions\Fixtures\TestException; class ExceptionHandlerTest extends TestCase { @@ -91,6 +99,38 @@ public function testAllReportersShouldBeCalled(): void $this->assertTrue(true); } + #[DataProvider('nonReportableExceptionsDataProvider')] + public function testNonReportableExceptions(\Throwable $exception): void + { + $reporter = $this->createMock(ExceptionReporterInterface::class); + $reporter->expects($this->never())->method('report'); + + $handler = $this->makeEmptyErrorHandler(); + $handler->addReporter($reporter); + + $handler->report($exception); + } + + public function testAddNonReportableExceptions(): void + { + $handler = $this->makeEmptyErrorHandler(); + $ref = new \ReflectionProperty($handler, 'nonReportableExceptions'); + $this->assertSame([ + ClientException::class, + AuthorizationException::class, + ValidationException::class, + ], $ref->getValue($handler)); + + $handler->dontReport(\DomainException::class); + + $this->assertSame([ + ClientException::class, + AuthorizationException::class, + ValidationException::class, + \DomainException::class + ], $ref->getValue($handler)); + } + private function makeEmptyErrorHandler(): ExceptionHandler { return new class extends ExceptionHandler { @@ -104,4 +144,20 @@ private function makeErrorHandler(): ExceptionHandler { return new ExceptionHandler(); } + + public static function nonReportableExceptionsDataProvider(): \Traversable + { + yield [new class extends ClientException {}]; + yield [new NotFoundException()]; + yield [new class extends NotFoundException {}]; + yield [new ForbiddenException()]; + yield [new class extends ForbiddenException {}]; + yield [new UnauthorizedException()]; + yield [new class extends UnauthorizedException {}]; + yield [new AuthorizationException()]; + yield [new class extends AuthorizationException {}]; + yield [new ValidationException([])]; + yield [new class([]) extends ValidationException {}]; + yield [new TestException()]; + } } diff --git a/src/Exceptions/tests/Fixtures/TestException.php b/src/Exceptions/tests/Fixtures/TestException.php new file mode 100644 index 000000000..9135db853 --- /dev/null +++ b/src/Exceptions/tests/Fixtures/TestException.php @@ -0,0 +1,12 @@ +assertSame(401, $e->getCode()); } - public function testServerError(): void - { - $e = new ServerErrorException(); - $this->assertSame(500, $e->getCode()); - } - #[DataProvider('allExceptionsWithPreviousSet')] public function testPreviousSetter(\Throwable $exception): void { @@ -63,7 +56,6 @@ public static function allExceptionsWithPreviousSet(): \Generator yield [new Exception\ClientException\BadRequestException('', new \Exception())]; yield [new Exception\ClientException\ForbiddenException('', new \Exception())]; yield [new Exception\ClientException\NotFoundException('', new \Exception())]; - yield [new Exception\ClientException\ServerErrorException('', new \Exception())]; yield [new Exception\ClientException\UnauthorizedException('', new \Exception())]; yield [new Exception\ClientException(0, '', new \Exception())]; yield [new Exception\DotNotFoundException('', 0, new \Exception())];