@@ -75,6 +75,11 @@ final class ErrorHandler
7575 private bool $ enabled = false ;
7676 private ?int $ originalErrorReportingLevel = null ;
7777
78+ /**
79+ * @var ?array{int, string, string, int}
80+ */
81+ private ?array $ forwardedError = null ;
82+
7883 /**
7984 * @var ?callable
8085 */
@@ -141,6 +146,15 @@ private function __construct(bool $identifyIssueTrigger)
141146 */
142147 public function __invoke (int $ errorNumber , string $ errorString , string $ errorFile , int $ errorLine ): bool
143148 {
149+ /**
150+ * A previously registered error handler may delegate an error that is being
151+ * forwarded to it back to this error handler: the issue must only be recorded
152+ * by the invocation that forwards the error, not by the delegating invocation.
153+ */
154+ if ($ this ->forwardedError === [$ errorNumber , $ errorString , $ errorFile , $ errorLine ]) {
155+ return false ;
156+ }
157+
144158 $ suppressed = (error_reporting () & ~self ::INSUPPRESSIBLE_LEVELS ) === 0 ;
145159
146160 if ($ suppressed && $ this ->excludeList ->isExcluded ($ errorFile )) {
@@ -288,6 +302,15 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil
288302
289303 public function handleNonTestCaseIssue (int $ errorNumber , string $ errorString , string $ errorFile , int $ errorLine ): true
290304 {
305+ /**
306+ * A previously registered error handler may delegate an error that is being
307+ * forwarded to it back to this error handler: the issue must only be recorded
308+ * by the invocation that forwards the error, not by the delegating invocation.
309+ */
310+ if ($ this ->forwardedError === [$ errorNumber , $ errorString , $ errorFile , $ errorLine ]) {
311+ return true ;
312+ }
313+
291314 $ suppressed = (error_reporting () & ~self ::INSUPPRESSIBLE_LEVELS ) === 0 ;
292315
293316 if ($ suppressed && $ this ->excludeList ->isExcluded ($ errorFile )) {
@@ -302,9 +325,7 @@ public function handleNonTestCaseIssue(int $errorNumber, string $errorString, st
302325
303326 if ($ errorString === '' || $ errorFile === '' || $ errorLine < 1 ) {
304327 // @codeCoverageIgnoreStart
305- if ($ this ->previousNonTestCaseErrorHandler !== null ) {
306- ($ this ->previousNonTestCaseErrorHandler )($ errorNumber , $ errorString , $ errorFile , $ errorLine );
307- }
328+ $ this ->forwardToPreviousNonTestCaseErrorHandler ($ errorNumber , $ errorString , $ errorFile , $ errorLine );
308329
309330 return true ;
310331 // @codeCoverageIgnoreEnd
@@ -317,9 +338,7 @@ public function handleNonTestCaseIssue(int $errorNumber, string $errorString, st
317338 *
318339 * @see https://github.com/sebastianbergmann/phpunit/issues/6817
319340 */
320- if ($ this ->previousNonTestCaseErrorHandler !== null ) {
321- ($ this ->previousNonTestCaseErrorHandler )($ errorNumber , $ errorString , $ errorFile , $ errorLine );
322- }
341+ $ this ->forwardToPreviousNonTestCaseErrorHandler ($ errorNumber , $ errorString , $ errorFile , $ errorLine );
323342
324343 /**
325344 * E_STRICT is deprecated since PHP 8.4.
@@ -1035,7 +1054,7 @@ private function deprecationIgnoredByFilter(string $message, string $file, int $
10351054
10361055 private function forwardToPreviousErrorHandler (int $ errorNumber , string $ errorString , string $ errorFile , int $ errorLine ): bool
10371056 {
1038- if ($ this ->previousErrorHandler === null ) {
1057+ if ($ this ->previousErrorHandler === null || $ this -> forwardedError !== null ) {
10391058 return false ;
10401059 }
10411060
@@ -1059,12 +1078,31 @@ private function forwardToPreviousErrorHandler(int $errorNumber, string $errorSt
10591078 $ restoreRequired = true ;
10601079 }
10611080
1081+ $ this ->forwardedError = [$ errorNumber , $ errorString , $ errorFile , $ errorLine ];
1082+
10621083 try {
10631084 return (bool ) ($ this ->previousErrorHandler )($ errorNumber , $ errorString , $ errorFile , $ errorLine );
10641085 } finally {
1086+ $ this ->forwardedError = null ;
1087+
10651088 if ($ restoreRequired ) {
10661089 error_reporting ($ errorReportingLevel );
10671090 }
10681091 }
10691092 }
1093+
1094+ private function forwardToPreviousNonTestCaseErrorHandler (int $ errorNumber , string $ errorString , string $ errorFile , int $ errorLine ): void
1095+ {
1096+ if ($ this ->previousNonTestCaseErrorHandler === null || $ this ->forwardedError !== null ) {
1097+ return ;
1098+ }
1099+
1100+ $ this ->forwardedError = [$ errorNumber , $ errorString , $ errorFile , $ errorLine ];
1101+
1102+ try {
1103+ ($ this ->previousNonTestCaseErrorHandler )($ errorNumber , $ errorString , $ errorFile , $ errorLine );
1104+ } finally {
1105+ $ this ->forwardedError = null ;
1106+ }
1107+ }
10701108}
0 commit comments