@@ -41,10 +41,17 @@ final class ErrorHandler
4141 private bool $ enabled = false ;
4242 private bool $ initialized = false ;
4343
44+ /**
45+ * @param LoggerInterface $logger Logger to write errors to.
46+ * @param ThrowableRendererInterface $defaultRenderer Default throwable renderer.
47+ * @param EventDispatcherInterface|null $eventDispatcher Event dispatcher for error events.
48+ * @param int $exitShutdownHandlerDepth Depth of the exit() shutdown handler to ensure it's executed last.
49+ */
4450 public function __construct (
4551 private LoggerInterface $ logger ,
4652 private ThrowableRendererInterface $ defaultRenderer ,
4753 private ?EventDispatcherInterface $ eventDispatcher = null ,
54+ private int $ exitShutdownHandlerDepth = 2
4855 ) {
4956 }
5057
@@ -108,7 +115,7 @@ public function register(): void
108115
109116 $ this ->initializeOnce ();
110117
111- // Handles throwable, echo output and exit.
118+ // Handles throwable that isn't caught otherwise , echo output and exit.
112119 set_exception_handler (function (Throwable $ t ): void {
113120 if (!$ this ->enabled ) {
114121 return ;
@@ -199,16 +206,41 @@ private function renderThrowableAndTerminate(Throwable $t): void
199206 if (!empty ($ this ->workingDirectory )) {
200207 chdir ($ this ->workingDirectory );
201208 }
202- // disable error capturing to avoid recursive errors while handling exceptions
209+ // Disable error capturing to avoid recursive errors while handling exceptions.
203210 $ this ->unregister ();
204- // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
211+ // Set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent.
205212 http_response_code (Status::INTERNAL_SERVER_ERROR );
206213
207214 echo $ this ->handle ($ t );
208215 $ this ->eventDispatcher ?->dispatch(new ApplicationError ($ t ));
209216
210- register_shutdown_function (static function (): void {
211- exit (1 );
212- });
217+ $ handler = $ this ->wrapShutdownHandler (
218+ static function (): void {
219+ exit (1 );
220+ },
221+ $ this ->exitShutdownHandlerDepth
222+ );
223+
224+ register_shutdown_function ($ handler );
225+ }
226+
227+ /**
228+ * Wraps shutdown handler into another shutdown handler to ensure it is called last after all other shutdown
229+ * functions, even those added to the end.
230+ *
231+ * @param callable $handler Shutdown handler to wrap.
232+ * @param int $depth Wrapping depth.
233+ * @return callable Wrapped handler.
234+ */
235+ private function wrapShutdownHandler (callable $ handler , int $ depth ): callable
236+ {
237+ $ currentDepth = 0 ;
238+ while ($ currentDepth < $ depth ) {
239+ $ handler = static function () use ($ handler ): void {
240+ register_shutdown_function ($ handler );
241+ };
242+ $ currentDepth ++;
243+ }
244+ return $ handler ;
213245 }
214246}
0 commit comments