diff --git a/src/Http/Http.php b/src/Http/Http.php index c0317508..7569a23d 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -637,6 +637,53 @@ public function match(Request $request, bool $fresh = false): ?Route return $this->route; } + /** + * Capture the current route context so nested dispatches can restore it. + * + * @return array + */ + private function captureRouteContext(): array + { + return [ + 'route' => $this->route, + 'hasRouteResource' => \array_key_exists('route', $this->resources), + 'routeResource' => $this->resources['route'] ?? null, + 'hasRouteResourceCallback' => \array_key_exists('route', self::$resourcesCallbacks), + 'routeResourceCallback' => self::$resourcesCallbacks['route'] ?? null, + ]; + } + + private function applyRouteContext(?Route $route, Request $request): void + { + $this->route = $route; + + self::setResource('route', function () use ($route, $request) { + return $route ?? new Route($request->getMethod(), $request->getURI()); + }); + } + + /** + * Restore the previous route context after nested dispatch completes. + * + * @param array $context + */ + private function restoreRouteContext(array $context): void + { + $this->route = $context['route']; + + if ($context['hasRouteResource']) { + $this->resources['route'] = $context['routeResource']; + } else { + unset($this->resources['route']); + } + + if ($context['hasRouteResourceCallback']) { + self::$resourcesCallbacks['route'] = $context['routeResourceCallback']; + } else { + unset(self::$resourcesCallbacks['route']); + } + } + /** * Execute a given route with middlewares and error handling * @@ -914,86 +961,102 @@ private function runInternal(Request $request, Response $response): static return $this; } - $method = $request->getMethod(); - $route = $this->match($request); - $this->matchedRoute = $route; - $groups = ($route instanceof Route) ? $route->getGroups() : []; - - if (null === $route && null !== self::$wildcardRoute) { - $route = self::$wildcardRoute; - $this->route = $route; - $path = \parse_url($request->getURI(), PHP_URL_PATH); - $route->path($path); - } + $routeContext = $this->captureRouteContext(); + $matchedRoute = $this->matchedRoute; - self::setResource('route', function () use ($route, $request) { - return $route ?? new Route($request->getMethod(), $request->getURI()); - }); + try { + $method = $request->getMethod(); + $route = $this->match($request, fresh: true); + $this->matchedRoute = $route; + $groups = ($route instanceof Route) ? $route->getGroups() : []; + + if (null === $route && null !== self::$wildcardRoute) { + $route = self::$wildcardRoute; + $path = \parse_url($request->getURI(), PHP_URL_PATH); + $route->path($path); + } - if (self::REQUEST_METHOD_HEAD == $method) { - $method = self::REQUEST_METHOD_GET; - $response->disablePayload(); - } + $this->applyRouteContext($route, $request); - if (self::REQUEST_METHOD_OPTIONS == $method) { - try { - foreach ($groups as $group) { - foreach (self::$options as $option) { // Group options hooks - /** @var Hook $option */ - if (in_array($group, $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + if (self::REQUEST_METHOD_HEAD == $method) { + $method = self::REQUEST_METHOD_GET; + $response->disablePayload(); + } + + if (self::REQUEST_METHOD_OPTIONS == $method) { + try { + foreach ($groups as $group) { + foreach (self::$options as $option) { // Group options hooks + /** @var Hook $option */ + if (in_array($group, $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + } } } - } - foreach (self::$options as $option) { // Global options hooks - /** @var Hook $option */ - if (in_array('*', $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + foreach (self::$options as $option) { // Global options hooks + /** @var Hook $option */ + if (in_array('*', $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + } } - } - } catch (\Throwable $e) { - foreach (self::$errors as $error) { // Global error hooks - /** @var Hook $error */ - if (in_array('*', $error->getGroups())) { - self::setResource('error', function () use ($e) { - return $e; - }); - try { - $arguments = $this->getArguments($error, [], $request->getParams()); - \call_user_func_array($error->getAction(), $arguments); - } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); + } catch (\Throwable $e) { + foreach (self::$errors as $error) { // Global error hooks + /** @var Hook $error */ + if (in_array('*', $error->getGroups())) { + self::setResource('error', function () use ($e) { + return $e; + }); + try { + $arguments = $this->getArguments($error, [], $request->getParams()); + \call_user_func_array($error->getAction(), $arguments); + } catch (\Throwable $e) { + throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); + } } } } + + return $this; } - return $this; - } + if (null !== $route) { + return $this->execute($route, $request, $response); + } elseif (self::REQUEST_METHOD_OPTIONS == $method) { + try { + foreach ($groups as $group) { + foreach (self::$options as $option) { // Group options hooks + if (in_array($group, $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + } + } + } - if (null !== $route) { - return $this->execute($route, $request, $response); - } elseif (self::REQUEST_METHOD_OPTIONS == $method) { - try { - foreach ($groups as $group) { - foreach (self::$options as $option) { // Group options hooks - if (in_array($group, $option->getGroups())) { + foreach (self::$options as $option) { // Global options hooks + if (in_array('*', $option->getGroups())) { \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); } } - } - - foreach (self::$options as $option) { // Global options hooks - if (in_array('*', $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + } catch (\Throwable $e) { + foreach (self::$errors as $error) { // Global error hooks + if (in_array('*', $error->getGroups())) { + self::setResource('error', function () use ($e) { + return $e; + }); + try { + $arguments = $this->getArguments($error, [], $request->getParams()); + \call_user_func_array($error->getAction(), $arguments); + } catch (\Throwable $e) { + throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); + } + } } } - } catch (\Throwable $e) { + } else { foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { - self::setResource('error', function () use ($e) { - return $e; + self::setResource('error', function () { + return new Exception('Not Found', 404); }); try { $arguments = $this->getArguments($error, [], $request->getParams()); @@ -1004,23 +1067,12 @@ private function runInternal(Request $request, Response $response): static } } } - } else { - foreach (self::$errors as $error) { // Global error hooks - if (in_array('*', $error->getGroups())) { - self::setResource('error', function () { - return new Exception('Not Found', 404); - }); - try { - $arguments = $this->getArguments($error, [], $request->getParams()); - \call_user_func_array($error->getAction(), $arguments); - } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); - } - } - } - } - return $this; + return $this; + } finally { + $this->restoreRouteContext($routeContext); + $this->matchedRoute = $matchedRoute; + } } /** diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 6e83993f..0328de69 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -577,6 +577,70 @@ public function testRouteInjectionAvailableInError(): void $this->assertSame('GET', $headers['X-Error-Method'] ?? null); } + public function testNestedInternalDispatchRestoresRouteContext(): void + { + $request = (new Request()) + ->setMethod('GET') + ->setURI('/outer-route'); + + Http::error() + ->inject('utopia') + ->inject('route') + ->inject('response') + ->action(function (Http $app, Route $route, Response $response) { + $response + ->addHeader('X-Inner-Error-Route', $route->getPath()) + ->addHeader('X-Inner-Error-Current-Route', $app->getRoute()?->getPath()); + }); + + Http::get('/inner-route') + ->inject('utopia') + ->inject('route') + ->inject('response') + ->action(function (Http $app, Route $route, Response $response) { + $response + ->addHeader('X-Inner-Route', $route->getPath()) + ->addHeader('X-Inner-Current-Route', $app->getRoute()?->getPath()); + + throw new Exception('Inner route failure'); + }); + + Http::get('/outer-route') + ->inject('utopia') + ->inject('route') + ->inject('response') + ->action(function (Http $app, Route $route, Response $response) { + $response + ->addHeader('X-Outer-Before-Route', $route->getPath()) + ->addHeader('X-Outer-Before-Current-Route', $app->getRoute()?->getPath()) + ->addHeader('X-Outer-Before-Resource-Route', $app->getResource('route')->getPath()); + + $innerRequest = (new Request()) + ->setMethod('GET') + ->setURI('/inner-route'); + + $app->run($innerRequest, $response); + + $response + ->addHeader('X-Outer-After-Current-Route', $app->getRoute()?->getPath()) + ->addHeader('X-Outer-After-Resource-Route', $app->getResource('route')->getPath()); + }); + + $response = new Response(); + $this->app->run($request, $response); + $headers = $response->getHeaders(); + + $this->assertSame('/outer-route', $headers['X-Outer-Before-Route'] ?? null); + $this->assertSame('/outer-route', $headers['X-Outer-Before-Current-Route'] ?? null); + $this->assertSame('/outer-route', $headers['X-Outer-Before-Resource-Route'] ?? null); + $this->assertSame('/inner-route', $headers['X-Inner-Route'] ?? null); + $this->assertSame('/inner-route', $headers['X-Inner-Current-Route'] ?? null); + $this->assertSame('/inner-route', $headers['X-Inner-Error-Route'] ?? null); + $this->assertSame('/inner-route', $headers['X-Inner-Error-Current-Route'] ?? null); + $this->assertSame('/outer-route', $headers['X-Outer-After-Current-Route'] ?? null); + $this->assertSame('/outer-route', $headers['X-Outer-After-Resource-Route'] ?? null); + } + public function providerRouteMatching(): array { return [