1818
1919use function in_array ;
2020use function is_a ;
21+ use function preg_quote ;
22+ use function preg_replace ;
2123use function rawurldecode ;
2224use function rtrim ;
2325use function set_error_handler ;
2931final class DispatchContext implements ContainerInterface
3032{
3133 /** @var array<int, mixed> */
32- public array $ params = [];
34+ public private(set) array $ params = [];
3335
34- public AbstractRoute |null $ route = null ;
36+ public private(set) AbstractRoute |null $ route = null ;
3537
3638 /** @var array<string, string> Headers to apply only when the response does not already have them */
37- public array $ defaultResponseHeaders = [];
38-
39- private RoutinePipeline |null $ routinePipeline = null ;
39+ public private(set) array $ defaultResponseHeaders = [];
4040
4141 private Responder |null $ responder = null ;
4242
@@ -49,21 +49,32 @@ final class DispatchContext implements ContainerInterface
4949
5050 private bool $ hasStatusOverride = false ;
5151
52- private string $ effectiveMethod = '' ;
53-
54- private string $ effectivePath = '' ;
52+ private string $ effectiveMethod ;
5553
56- /** @var array<int, AbstractRoute> */
57- private array $ handlers = [];
54+ private string $ effectivePath ;
5855
5956 private Resolver |null $ resolver = null ;
6057
58+ /** @param array<int, AbstractRoute> $handlers */
6159 public function __construct (
62- public ServerRequestInterface $ request ,
63- public ResponseFactoryInterface &StreamFactoryInterface $ factory ,
60+ public private(set ) ServerRequestInterface $ request ,
61+ public private(set ) ResponseFactoryInterface &StreamFactoryInterface $ factory ,
62+ private RoutinePipeline $ routinePipeline = new RoutinePipeline (),
63+ private array $ handlers = [],
64+ string $ basePath = '' ,
6465 ) {
65- $ this ->effectivePath = rtrim (rawurldecode ($ request ->getUri ()->getPath ()), ' / ' );
66+ $ path = rtrim (rawurldecode ($ request ->getUri ()->getPath ()), ' / ' );
67+ if ($ basePath !== '' ) {
68+ $ path = preg_replace (
69+ '#^ ' . preg_quote ($ basePath , '# ' ) . '# ' ,
70+ '' ,
71+ $ path ,
72+ ) ?? $ path ;
73+ }
74+
75+ $ this ->effectivePath = $ path ;
6676 $ this ->effectiveMethod = strtoupper ($ request ->getMethod ());
77+ $ this ->resetHandlerState ();
6778 }
6879
6980 public function method (): string
@@ -76,11 +87,6 @@ public function path(): string
7687 return $ this ->effectivePath ;
7788 }
7889
79- public function setPath (string $ path ): void
80- {
81- $ this ->effectivePath = $ path ;
82- }
83-
8490 public function hasPreparedResponse (): bool
8591 {
8692 return $ this ->hasPreparedResponse ;
@@ -125,6 +131,13 @@ public function prepareResponse(int $status, array $headers = []): void
125131 }
126132 }
127133
134+ /** @param array<int, mixed> $params */
135+ public function configureRoute (AbstractRoute $ route , array $ params = []): void
136+ {
137+ $ this ->route = $ route ;
138+ $ this ->params = $ params ;
139+ }
140+
128141 /** Generates the PSR-7 response from the current route */
129142 public function response (): ResponseInterface |null
130143 {
@@ -146,7 +159,7 @@ public function response(): ResponseInterface|null
146159 $ previousErrorHandler = $ isHandler ? null : $ this ->installErrorHandler ();
147160
148161 try {
149- $ preRoutineResult = $ this ->routinePipeline () ->processBy ($ this , $ route );
162+ $ preRoutineResult = $ this ->routinePipeline ->processBy ($ this , $ route );
150163
151164 if ($ preRoutineResult instanceof AbstractRoute) {
152165 return $ this ->forward ($ preRoutineResult );
@@ -166,7 +179,7 @@ public function response(): ResponseInterface|null
166179 return $ this ->forward ($ rawResult );
167180 }
168181
169- $ processedResult = $ this ->routinePipeline () ->processThrough ($ this , $ route , $ rawResult );
182+ $ processedResult = $ this ->routinePipeline ->processThrough ($ this , $ route , $ rawResult );
170183
171184 if (!$ isHandler ) {
172185 $ errorResponse = $ this ->forwardCollectedErrors ();
@@ -192,27 +205,21 @@ public function response(): ResponseInterface|null
192205 }
193206 }
194207
195- public function forward ( AbstractRoute $ route ): ResponseInterface | null
208+ public function withRequest ( ServerRequestInterface $ request ): void
196209 {
197- $ this ->route = $ route ;
198-
199- return $ this ->response ();
210+ $ this ->request = $ request ;
200211 }
201212
202- public function setRoutinePipeline ( RoutinePipeline $ routinePipeline ): void
213+ public function setResponder ( Responder $ responder ): void
203214 {
204- $ this ->routinePipeline = $ routinePipeline ;
215+ $ this ->responder = $ responder ;
205216 }
206217
207- /** @param array<int, AbstractRoute> $handlers */
208- public function setHandlers (array $ handlers ): void
218+ public function forward (AbstractRoute $ route ): ResponseInterface |null
209219 {
210- $ this ->handlers = $ handlers ;
211- }
220+ $ this ->route = $ route ;
212221
213- public function setResponder (Responder $ responder ): void
214- {
215- $ this ->responder = $ responder ;
222+ return $ this ->response ();
216223 }
217224
218225 public function resolver (): Resolver
@@ -239,7 +246,26 @@ public function get(string $id): mixed
239246 throw new NotFoundException (sprintf ('No entry found for "%s" ' , $ id ));
240247 }
241248
242- /** @return callable|null The previous error handler, or null if no ErrorHandler is registered */
249+ private function resetHandlerState (): void
250+ {
251+ foreach ($ this ->handlers as $ handler ) {
252+ if ($ handler instanceof ErrorHandler) {
253+ $ handler ->errors = [];
254+ } elseif ($ handler instanceof ExceptionHandler) {
255+ $ handler ->exception = null ;
256+ }
257+ }
258+ }
259+
260+ /**
261+ * Installs a custom error handler that collects PHP errors for the current dispatch.
262+ *
263+ * Safe for single-request-per-worker runtimes (PHP-FPM, Swoole workers,
264+ * FrankenPHP workers, ReactPHP). Not safe for coroutine-concurrent request
265+ * handling within a single PHP process, since set_error_handler is global state.
266+ *
267+ * @return callable|null The previous error handler, or null if no ErrorHandler is registered
268+ */
243269 private function installErrorHandler (): callable |null
244270 {
245271 foreach ($ this ->handlers as $ handler ) {
@@ -303,7 +329,7 @@ private function forwardToStatusRoute(ResponseInterface $preparedResponse): Resp
303329
304330 // Run routine negotiation (e.g. Accept) before forwarding,
305331 // since the normal route-selection phase was skipped
306- $ this ->routinePipeline () ->matches ($ this , $ handler , $ this ->params );
332+ $ this ->routinePipeline ->matches ($ this , $ handler , $ this ->params );
307333
308334 $ result = $ this ->forward ($ handler );
309335
@@ -327,27 +353,14 @@ private function finalizeResponse(mixed $response): ResponseInterface
327353 );
328354 }
329355
330- private function routinePipeline (): RoutinePipeline
331- {
332- return $ this ->routinePipeline ??= new RoutinePipeline ();
333- }
334-
335356 private function responder (): Responder
336357 {
337- if ($ this ->responder !== null ) {
338- return $ this ->responder ;
339- }
340-
341- return $ this ->responder = new Responder ($ this ->factory );
358+ return $ this ->responder ??= new Responder ($ this ->factory );
342359 }
343360
344361 private function ensureResponseDraft (): ResponseInterface
345362 {
346- if ($ this ->responseDraft !== null ) {
347- return $ this ->responseDraft ;
348- }
349-
350- return $ this ->responseDraft = $ this ->factory ->createResponse ();
363+ return $ this ->responseDraft ??= $ this ->factory ->createResponse ();
351364 }
352365
353366 public function __toString (): string
0 commit comments