Skip to content

Commit 2ba2fa4

Browse files
loks0nclaude
andcommitted
Make execute(Request, Response) the public dispatch entry point
Aligns the API with how callers think about it: Route is a definition, RouteMatch is the immutable result of matching, execute() is the verb that ties them together (match -> resolve -> run). - Http::execute now takes (Request, Response) and does match + dispatch internally, including OPTIONS/HEAD handling and 404 fallback. Replaces the prior shape that required callers to pre-build a RouteMatch. - Http::match becomes stateless: drop the $fresh / context-cache that silently returned the previous request's match when a caller invoked execute() multiple times with different requests. - runInternal collapses to: pre-checks (compression, request hooks, static files) + delegate to execute(). - Update tests: hand-built routes now register via Http::get/post/etc., set $_SERVER['REQUEST_URI'] before execute(), and use Http::setAllowOverride(true) for tests that re-register the same path. - Update Router::match callers to unwrap ->route from RouteMatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 291326e commit 2ba2fa4

4 files changed

Lines changed: 188 additions & 185 deletions

File tree

src/Http/Http.php

Lines changed: 64 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -532,23 +532,13 @@ public function start(): void
532532
}
533533

534534
/**
535-
* Match
535+
* Find the matching route for a request, or null if none match.
536536
*
537-
* Find matching route given current user request
538-
*
539-
* @param bool $fresh If true, will not match any cached route
537+
* Stateless: re-runs the lookup every call, so callers always see a
538+
* result reflecting the request they passed in.
540539
*/
541-
public function match(Request $request, bool $fresh = true): ?RouteMatch
540+
public function match(Request $request): ?RouteMatch
542541
{
543-
$context = $this->context();
544-
545-
if (!$fresh && $context->has('match')) {
546-
$cached = $context->get('match');
547-
if ($cached instanceof RouteMatch) {
548-
return $cached;
549-
}
550-
}
551-
552542
$url = parse_url($request->getURI(), PHP_URL_PATH);
553543
$url = \is_string($url) ? ($url === '' ? '/' : $url) : '/';
554544
$method = $request->getMethod();
@@ -560,18 +550,73 @@ public function match(Request $request, bool $fresh = true): ?RouteMatch
560550
return null;
561551
}
562552

563-
$context->set('match', fn() => $match, []);
564553
$route = $match->route;
565-
$context->set('route', fn() => $route, []);
554+
$this->context()->set('route', fn() => $route, []);
566555

567556
return $match;
568557
}
569558

570559
/**
571-
* Execute a matched route with middlewares and error handling.
560+
* Match the request to a registered route, then run its handler and hooks.
561+
*
562+
* Handles OPTIONS preflight (fires options hooks, returns) and HEAD
563+
* (matches as GET, suppresses the response body). If no route matches and
564+
* the method isn't OPTIONS, fires global error hooks with a 404 Exception.
572565
*/
573-
public function execute(RouteMatch $match, Request $request, Response $response): static
566+
public function execute(Request $request, Response $response): static
574567
{
568+
$method = $request->getMethod();
569+
570+
if (self::REQUEST_METHOD_HEAD === $method) {
571+
$method = self::REQUEST_METHOD_GET;
572+
$response->disablePayload();
573+
}
574+
575+
$match = $this->match($request);
576+
577+
if (self::REQUEST_METHOD_OPTIONS === $method) {
578+
$groups = $match?->route->getGroups() ?? [];
579+
580+
try {
581+
foreach ($groups as $group) {
582+
foreach (self::$options as $option) { // Group options hooks
583+
/** @var Hook $option */
584+
if (\in_array($group, $option->getGroups())) {
585+
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
586+
}
587+
}
588+
}
589+
590+
foreach (self::$options as $option) { // Global options hooks
591+
/** @var Hook $option */
592+
if (\in_array('*', $option->getGroups())) {
593+
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
594+
}
595+
}
596+
} catch (\Throwable $e) {
597+
foreach (self::$errors as $error) { // Global error hooks
598+
/** @var Hook $error */
599+
if (\in_array('*', $error->getGroups())) {
600+
$this->context()->set('error', fn() => $e, []);
601+
\call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams()));
602+
}
603+
}
604+
}
605+
606+
return $this;
607+
}
608+
609+
if ($match === null) {
610+
foreach (self::$errors as $error) {
611+
if (\in_array('*', $error->getGroups())) {
612+
$this->context()->set('error', fn() => new Exception('Not Found', 404), []);
613+
\call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams()));
614+
}
615+
}
616+
617+
return $this;
618+
}
619+
575620
$route = $match->route;
576621
$arguments = [];
577622
$groups = $route->getGroups();
@@ -799,82 +844,7 @@ private function runInternal(Request $request, Response $response): static
799844
return $this;
800845
}
801846

802-
$method = $request->getMethod();
803-
$match = $this->match($request);
804-
$groups = $match instanceof RouteMatch ? $match->route->getGroups() : [];
805-
806-
if (self::REQUEST_METHOD_HEAD === $method) {
807-
$method = self::REQUEST_METHOD_GET;
808-
$response->disablePayload();
809-
}
810-
811-
if (self::REQUEST_METHOD_OPTIONS === $method) {
812-
try {
813-
foreach ($groups as $group) {
814-
foreach (self::$options as $option) { // Group options hooks
815-
/** @var Hook $option */
816-
if (\in_array($group, $option->getGroups())) {
817-
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
818-
}
819-
}
820-
}
821-
822-
foreach (self::$options as $option) { // Global options hooks
823-
/** @var Hook $option */
824-
if (\in_array('*', $option->getGroups())) {
825-
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
826-
}
827-
}
828-
} catch (\Throwable $e) {
829-
foreach (self::$errors as $error) { // Global error hooks
830-
/** @var Hook $error */
831-
if (\in_array('*', $error->getGroups())) {
832-
$this->context()->set('error', fn() => $e, []);
833-
\call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams()));
834-
}
835-
}
836-
}
837-
838-
return $this;
839-
}
840-
841-
if ($match instanceof RouteMatch) {
842-
return $this->execute($match, $request, $response);
843-
}
844-
845-
if (self::REQUEST_METHOD_OPTIONS === $method) {
846-
try {
847-
foreach ($groups as $group) {
848-
foreach (self::$options as $option) { // Group options hooks
849-
if (\in_array($group, $option->getGroups())) {
850-
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
851-
}
852-
}
853-
}
854-
855-
foreach (self::$options as $option) { // Global options hooks
856-
if (\in_array('*', $option->getGroups())) {
857-
\call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams()));
858-
}
859-
}
860-
} catch (\Throwable $e) {
861-
foreach (self::$errors as $error) { // Global error hooks
862-
if (\in_array('*', $error->getGroups())) {
863-
$this->context()->set('error', fn() => $e, []);
864-
\call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams()));
865-
}
866-
}
867-
}
868-
} else {
869-
foreach (self::$errors as $error) { // Global error hooks
870-
if (\in_array('*', $error->getGroups())) {
871-
$this->context()->set('error', fn() => new Exception('Not Found', 404), []);
872-
\call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams()));
873-
}
874-
}
875-
}
876-
877-
return $this;
847+
return $this->execute($request, $response);
878848
}
879849

880850

src/Http/RouteMatch.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,16 @@ public function __construct(
3030
public string $path,
3131
) {
3232
}
33+
34+
/**
35+
* Wrap a Route with no matched template — for invoking a Route's handler
36+
* outside the routing pipeline (e.g. in tests). Path-param resolution
37+
* falls back to the Route's first registered template, which is correct
38+
* iff the Route has no aliases. Routed callers should construct a full
39+
* RouteMatch via {@see Router::match()} to pick up the right template.
40+
*/
41+
public static function for(Route $route): self
42+
{
43+
return new self($route, '');
44+
}
3345
}

0 commit comments

Comments
 (0)