Commit 17f3d5e
Make routing coroutine-safe and tighten the routing API (#255)
* Make routing coroutine-safe by removing Route mutations
Router::match and the wildcard branch in Http::runInternal both wrote
to the shared Route singleton (setMatchedPath, path) on every request.
Under Swoole coroutines the Route is shared across in-flight requests,
so concurrent requests could observe each other's matched path.
- Router::match now returns [Route, matchedPath] instead of mutating
the Route. A new Router::setFallback slot replaces Http::$wildcardRoute,
so the method-agnostic catch-all flows through the same matching path
as any other route.
- Route::matchedPath / setMatchedPath / getMatchedPath are removed.
- Http::execute takes the matched path as a parameter; runInternal
threads it through. Public Http::match keeps its ?Route shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Move matched route + matched path into request context
The Http instance is shared across coroutines, so $this->route and
$this->matchedPath would race the same way Route's mutable fields did.
Store them in the per-request context() container instead, which is
already request-scoped post-#254. getRoute()/setRoute() now read/write
through the context too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Inline context key strings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Rename Router fallback slot to wildcard
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Introduce Router\Result DTO for match results
Replace the [Route, matchedPath] tuple with a readonly Router\Result
value object so callers get named, typed access instead of positional
unpacking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop redundant preparePath in Http::execute
$matchedPath is already the prepared form (the key from Router::$routes),
so re-preparing it just returned the same string. Pass it straight to
Route::getPathValues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Rename Router\Result to RouteMatch, drop matchInternal indirection
- Move the value object to Utopia\Http\RouteMatch (top-level), since
'Match' is reserved by PHP 8.0+. RouteMatch is short, clear, and
doesn't shadow the keyword.
- Rename matchedPath -> path on the DTO; the field name is qualified by
the surrounding RouteMatch context.
- Inline matchInternal: public Http::match now returns ?RouteMatch
directly instead of indirecting through a private helper.
- Http::execute now takes a RouteMatch (route + matched path together)
instead of separate args, so callers can't pass mismatched pairs.
- Cache the whole RouteMatch under 'match' in the per-request context;
keep 'route' set too for downstream injection compat.
- Add per-property docblocks on RouteMatch.
- Update tests to wrap raw Routes in RouteMatch when calling execute().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
* RouteMatch carries resolved params, not the matched template
The matched-template string was purely instrumental — its only job was
to look up Route::pathParams[$template] so callers could resolve URL
segments into a name->value map. Now Router::match resolves the params
itself and stores them on RouteMatch directly, so dispatch is just
`$match->params` with no second-stage resolution.
- RouteMatch.path: string -> RouteMatch.params: array<string, string>.
- Route::getPathValues renamed to Route::resolveParams; takes a URL
string instead of a Request (the resolution doesn't need anything
else from the request).
- Router::match calls resolveParams at match time. Static and wildcard
matches pass [] for params.
- Http::execute drops getPathValues call; reads $match->params directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop Http::setRoute()
The matched route is owned by the routing pipeline and lives in the
per-request context. setRoute let arbitrary code overwrite it post-match
without invalidating any other state — a footgun under coroutines and
not used in production. Drop it; getRoute() remains as a read-only view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop Http::getRoute()
The supported way to consume the matched route is via the 'route'
injection inside hooks/actions:
Http::init()
->inject('route')
->action(function (?Route $route) { ... });
getRoute() was a convenience accessor on the shared Http instance.
Reading mutable per-request state through a method on a shared object
encourages racy patterns under coroutines (e.g. caching a Route
reference, calling getRoute() outside a request scope). Drop it; tests
that needed the matched route now consume it via the injection or via
the RouteMatch returned from match() directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Tighten doc comments to user-facing intent
Drop internal narrative about coroutine safety, mutation-vs-immutability,
and "we used to do X." Comments now describe what each public surface
does for someone calling it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Document run vs execute as distinct entry points
run() is the top-level request lifecycle (compression, request hooks,
static files, match, dispatch, telemetry) — wired into the server
adapter. execute() is the re-entrant dispatch primitive — match +
handler + hooks only — for sub-requests from inside a handler (e.g.
GraphQL resolvers synthesizing Request/Response pairs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Inline \$match->params in execute, drop \$pathValues alias
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop intermediate variables in run() telemetry
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Apply rector and pint to RouteMatch.php
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Save/restore context['route'] across execute() dispatch
match() no longer writes to context — that was leaking the inner
match into the outer's context whenever execute() was called for a
sub-request, breaking outer-request shutdown hooks doing
->inject('route') and the http.route telemetry attribute.
execute() now sets context['route'] right before dispatching and
restores the prior value (or null) in a finally clause, so nested
execute() calls don't trample each other's frame.
Adds testSubrequestRestoresOuterRoute as a regression test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Make 'route' injection frame-local instead of stateful
The save/restore pattern was just bookkeeping around a shared mutable
slot — anything else writing to context['route'] during dispatch would
break the restore, and a missed restore in any branch leaks the inner
match into the outer frame.
Drop context['route'] entirely. Pass the dispatch frame's Route through
to getArguments and special-case the 'route' injection there. Each
dispatch frame (including sub-requests via execute()) carries its own
matched Route as a parameter; nested calls can't trample each other
because there's no shared state to trample.
Telemetry in run() now reads the outer match by calling match() once
locally — match() is pure and cheap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Apply rector to HttpTest
assertEquals -> assertSame in testSubrequestRestoresOuterRoute.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Coerce empty wildcard path to null in telemetry
Route::getPath() returns '' for the wildcard route by construction
(registered as 'new Route("", "")'). Emitting that as the OTel
http.route attribute would set the attribute to an empty string —
different from "unset" in OTel semantics. Coerce to null at the
attribute boundary so http.route is unset for wildcard / no-match
requests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Wildcard is a Hook, not a Route
The wildcard fallback was modeled as `new Route('', '')` — a Route with
empty method and path, which are meaningless for a method-agnostic
catch-all. The empty strings were sentinels that leaked into
Route::getPath() at runtime for any handler doing inject('route').
Model honestly: Http::wildcard() returns a Hook (the parent class —
action + params + injections + groups). Router::\$wildcard is now
?Hook. RouteMatch::\$route is typed Hook so it can carry either —
consumers wanting Route-only fields (getMethod, getPath) check
instanceof Route first.
Behavior: inject('route') now correctly returns null inside a wildcard
handler (it never had a Route to begin with). Telemetry's http.route
attribute is unset for wildcard matches, matching OTel semantics.
Group hooks don't fire for wildcard matches (it has no groups beyond
'*'); the global init/shutdown blocks still run because there's no
Route::getHook() opt-out for the wildcard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "Wildcard is a Hook, not a Route"
This reverts commit e89d58f.
* Add 'params' frame-local injection
Mirrors the 'route' injection: hooks can do ->inject('params') to receive
the resolved path params (array<string, string>) for the current dispatch
frame. Skips shared context for the same reason 'route' does — nested
execute() calls (sub-request dispatch) can't trample each other.
Refactor: collapse the per-name special-cases into a frame-local map for
clarity. Both 'route' and 'params' come from the dispatch frame; the map
makes that explicit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Rename frameLocals to locals
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 3e3b431 commit 17f3d5e
6 files changed
Lines changed: 353 additions & 300 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | 101 | | |
115 | 102 | | |
116 | 103 | | |
| |||
242 | 229 | | |
243 | 230 | | |
244 | 231 | | |
245 | | - | |
| 232 | + | |
| 233 | + | |
246 | 234 | | |
247 | | - | |
| 235 | + | |
248 | 236 | | |
249 | 237 | | |
250 | 238 | | |
| |||
416 | 404 | | |
417 | 405 | | |
418 | 406 | | |
419 | | - | |
420 | | - | |
421 | | - | |
422 | | - | |
423 | | - | |
424 | | - | |
425 | | - | |
426 | | - | |
427 | | - | |
428 | | - | |
429 | | - | |
430 | | - | |
431 | | - | |
432 | | - | |
433 | | - | |
434 | | - | |
435 | | - | |
436 | | - | |
437 | 407 | | |
438 | 408 | | |
439 | 409 | | |
| |||
538 | 508 | | |
539 | 509 | | |
540 | 510 | | |
541 | | - | |
542 | | - | |
543 | | - | |
544 | | - | |
545 | | - | |
| 511 | + | |
546 | 512 | | |
547 | | - | |
| 513 | + | |
548 | 514 | | |
549 | | - | |
550 | | - | |
551 | | - | |
552 | | - | |
553 | 515 | | |
554 | 516 | | |
555 | 517 | | |
556 | 518 | | |
557 | 519 | | |
558 | | - | |
559 | | - | |
560 | | - | |
| 520 | + | |
561 | 521 | | |
562 | 522 | | |
563 | 523 | | |
564 | | - | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
565 | 535 | | |
566 | | - | |
| 536 | + | |
567 | 537 | | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
568 | 591 | | |
569 | 592 | | |
570 | 593 | | |
571 | | - | |
572 | | - | |
573 | | - | |
574 | 594 | | |
575 | 595 | | |
576 | 596 | | |
577 | 597 | | |
578 | | - | |
| 598 | + | |
579 | 599 | | |
580 | 600 | | |
581 | 601 | | |
| |||
584 | 604 | | |
585 | 605 | | |
586 | 606 | | |
587 | | - | |
| 607 | + | |
588 | 608 | | |
589 | 609 | | |
590 | 610 | | |
591 | 611 | | |
592 | 612 | | |
593 | 613 | | |
594 | | - | |
| 614 | + | |
595 | 615 | | |
596 | 616 | | |
597 | 617 | | |
598 | 618 | | |
599 | 619 | | |
600 | 620 | | |
601 | | - | |
| 621 | + | |
602 | 622 | | |
603 | 623 | | |
604 | 624 | | |
| |||
607 | 627 | | |
608 | 628 | | |
609 | 629 | | |
610 | | - | |
| 630 | + | |
611 | 631 | | |
612 | 632 | | |
613 | 633 | | |
| |||
619 | 639 | | |
620 | 640 | | |
621 | 641 | | |
622 | | - | |
| 642 | + | |
623 | 643 | | |
624 | 644 | | |
625 | 645 | | |
| |||
631 | 651 | | |
632 | 652 | | |
633 | 653 | | |
634 | | - | |
| 654 | + | |
635 | 655 | | |
636 | 656 | | |
637 | 657 | | |
| |||
651 | 671 | | |
652 | 672 | | |
653 | 673 | | |
654 | | - | |
| 674 | + | |
655 | 675 | | |
656 | 676 | | |
657 | 677 | | |
| |||
700 | 720 | | |
701 | 721 | | |
702 | 722 | | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
703 | 731 | | |
704 | | - | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
705 | 735 | | |
706 | 736 | | |
707 | 737 | | |
708 | 738 | | |
709 | 739 | | |
710 | 740 | | |
711 | | - | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
712 | 752 | | |
713 | 753 | | |
714 | 754 | | |
| |||
724 | 764 | | |
725 | 765 | | |
726 | 766 | | |
727 | | - | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
728 | 770 | | |
729 | 771 | | |
730 | 772 | | |
| |||
789 | 831 | | |
790 | 832 | | |
791 | 833 | | |
792 | | - | |
793 | | - | |
794 | | - | |
795 | | - | |
796 | | - | |
797 | | - | |
798 | | - | |
799 | | - | |
800 | | - | |
801 | | - | |
802 | | - | |
803 | | - | |
804 | | - | |
805 | | - | |
806 | | - | |
807 | | - | |
808 | | - | |
809 | | - | |
810 | | - | |
811 | | - | |
812 | | - | |
813 | | - | |
814 | | - | |
815 | | - | |
816 | | - | |
817 | | - | |
818 | | - | |
819 | | - | |
820 | | - | |
821 | | - | |
822 | | - | |
823 | | - | |
824 | | - | |
825 | | - | |
826 | | - | |
827 | | - | |
828 | | - | |
829 | | - | |
830 | | - | |
831 | | - | |
832 | | - | |
833 | | - | |
834 | | - | |
835 | | - | |
836 | | - | |
837 | | - | |
838 | | - | |
839 | | - | |
840 | | - | |
841 | | - | |
842 | | - | |
843 | | - | |
844 | | - | |
845 | | - | |
846 | | - | |
847 | | - | |
848 | | - | |
849 | | - | |
850 | | - | |
851 | | - | |
852 | | - | |
853 | | - | |
854 | | - | |
855 | | - | |
856 | | - | |
857 | | - | |
858 | | - | |
859 | | - | |
860 | | - | |
861 | | - | |
862 | | - | |
863 | | - | |
864 | | - | |
865 | | - | |
866 | | - | |
867 | | - | |
868 | | - | |
869 | | - | |
870 | | - | |
871 | | - | |
872 | | - | |
873 | | - | |
874 | | - | |
875 | | - | |
876 | | - | |
877 | | - | |
878 | | - | |
| 834 | + | |
879 | 835 | | |
880 | 836 | | |
881 | 837 | | |
| |||
923 | 879 | | |
924 | 880 | | |
925 | 881 | | |
926 | | - | |
927 | 882 | | |
928 | 883 | | |
0 commit comments