diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index d9010f2d1fcc..3b439655e991 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -42,6 +42,7 @@ use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Throwable; +use WeakMap; class Handler implements ExceptionHandlerContract { @@ -119,6 +120,20 @@ class Handler implements ExceptionHandlerContract 'password_confirmation', ]; + /** + * Indicates that exception reporting should be deduplicated. + * + * @var bool + */ + protected $deduplicateReporting = false; + + /** + * The already reported exception map. + * + * @var \WeakMap + */ + protected $reportedExceptionMap; + /** * Create a new exception handler instance. * @@ -129,6 +144,8 @@ public function __construct(Container $container) { $this->container = $container; + $this->reportedExceptionMap = new WeakMap; + $this->register(); } @@ -260,6 +277,8 @@ public function report(Throwable $e) */ protected function reportThrowable(Throwable $e): void { + $this->reportedExceptionMap[$e] = true; + if (Reflector::isCallable($reportCallable = [$e, 'report']) && $this->container->call($reportCallable) !== false) { return; @@ -307,6 +326,10 @@ public function shouldReport(Throwable $e) */ protected function shouldntReport(Throwable $e) { + if ($this->deduplicateReporting && ($this->reportedExceptionMap[$e] ?? false)) { + return true; + } + $dontReport = array_merge($this->dontReport, $this->internalDontReport); return ! is_null(Arr::first($dontReport, fn ($type) => $e instanceof $type)); @@ -786,6 +809,18 @@ public function renderForConsole($output, Throwable $e) (new ConsoleApplication)->renderThrowable($e, $output); } + /** + * Do not report duplicate exceptions. + * + * @return $this + */ + public function dontReportDuplicates() + { + $this->deduplicateReporting = true; + + return $this; + } + /** * Determine if the given exception is an HTTP exception. * diff --git a/tests/Foundation/FoundationExceptionsHandlerTest.php b/tests/Foundation/FoundationExceptionsHandlerTest.php index 328583473ed6..3b689fd0b809 100644 --- a/tests/Foundation/FoundationExceptionsHandlerTest.php +++ b/tests/Foundation/FoundationExceptionsHandlerTest.php @@ -436,6 +436,40 @@ public function testAssertExceptionIsThrown() Assert::fail('assertThrows failed: non matching message are thrown.'); } } + + public function testItReportsDuplicateExceptions() + { + $reported = []; + $this->handler->reportable(function (\Throwable $e) use (&$reported) { + $reported[] = $e; + + return false; + }); + + $this->handler->report($one = new RuntimeException('foo')); + $this->handler->report($one); + $this->handler->report($two = new RuntimeException('foo')); + + $this->assertSame($reported, [$one, $one, $two]); + } + + public function testItCanDedupeExceptions() + { + $reported = []; + $e = new RuntimeException('foo'); + $this->handler->reportable(function (\Throwable $e) use (&$reported) { + $reported[] = $e; + + return false; + }); + + $this->handler->dontReportDuplicates(); + $this->handler->report($one = new RuntimeException('foo')); + $this->handler->report($one); + $this->handler->report($two = new RuntimeException('foo')); + + $this->assertSame($reported, [$one, $two]); + } } class CustomException extends Exception