@@ -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 ;
@@ -200,16 +207,41 @@ private function renderThrowableAndTerminate(Throwable $t): void
200207 if (!empty ($ this ->workingDirectory )) {
201208 chdir ($ this ->workingDirectory );
202209 }
203- // disable error capturing to avoid recursive errors while handling exceptions
210+ // Disable error capturing to avoid recursive errors while handling exceptions.
204211 $ this ->unregister ();
205- // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
212+ // Set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent.
206213 http_response_code (Status::INTERNAL_SERVER_ERROR );
207214
208215 echo $ this ->handle ($ t );
209216 $ this ->eventDispatcher ?->dispatch(new ApplicationError ($ t ));
210217
211- register_shutdown_function (static function (): void {
212- exit (1 );
213- });
218+ $ handler = $ this ->wrapShutdownHandler (
219+ static function (): void {
220+ exit (1 );
221+ },
222+ $ this ->exitShutdownHandlerDepth
223+ );
224+
225+ register_shutdown_function ($ handler );
226+ }
227+
228+ /**
229+ * Wraps shutdown handler into another shutdown handler to ensure it is called last after all other shutdown
230+ * functions, even those added to the end.
231+ *
232+ * @param callable $handler Shutdown handler to wrap.
233+ * @param int $depth Wrapping depth.
234+ * @return callable Wrapped handler.
235+ */
236+ private function wrapShutdownHandler (callable $ handler , int $ depth ): callable
237+ {
238+ $ currentDepth = 0 ;
239+ while ($ currentDepth < $ depth ) {
240+ $ handler = static function () use ($ handler ): void {
241+ register_shutdown_function ($ handler );
242+ };
243+ $ currentDepth ++;
244+ }
245+ return $ handler ;
214246 }
215247}
0 commit comments