Skip to content

Commit c6770ad

Browse files
loks0nclaude
andcommitted
Add Router::matchRequest convenience entry point
Router::matchRoute() takes (method, path) strings, forcing every caller to hand-roll URL parsing and HEAD-to-GET normalisation. The library already does this internally in Dispatcher::handle; the matching boilerplate should not be the caller's problem. Router::matchRequest(Request) wraps matchRoute and handles both. The deprecated Http::match() shim now delegates to it; Dispatcher continues to call matchRoute directly because it needs the parsed URL in scope for the wildcard fallback anyway. Adds three regression tests covering URL extraction, root-URL fallback when the URI has no path, and HEAD normalisation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 07087af commit c6770ad

3 files changed

Lines changed: 70 additions & 10 deletions

File tree

src/Http/Http.php

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -531,19 +531,14 @@ public function start(): void
531531
}
532532

533533
/**
534-
* @deprecated Use {@see Router::matchRoute()} which returns a per-request
535-
* {@see RouteMatch}. This shim discards the `$fresh` argument: the
536-
* previous implementation cached the match on the Http singleton,
537-
* which is not safe under concurrent request handling.
534+
* @deprecated Use {@see Router::matchRequest()} which returns a
535+
* per-request {@see RouteMatch}. This shim discards the `$fresh`
536+
* argument: the previous implementation cached the match on the Http
537+
* singleton, which is not safe under concurrent request handling.
538538
*/
539539
public function match(Request $request, bool $fresh = true): ?Route
540540
{
541-
$url = \parse_url($request->getURI(), PHP_URL_PATH);
542-
$url = \is_string($url) ? ($url === '' ? '/' : $url) : '/';
543-
$method = $request->getMethod();
544-
$method = (self::REQUEST_METHOD_HEAD === $method) ? self::REQUEST_METHOD_GET : $method;
545-
546-
$match = Router::matchRoute($method, $url);
541+
$match = Router::matchRequest($request);
547542
if ($match === null) {
548543
return null;
549544
}

src/Http/Router.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,26 @@ public static function matchRoute(string $method, string $path): ?RouteMatch
159159
return null;
160160
}
161161

162+
/**
163+
* Match a {@see Request} against the router.
164+
*
165+
* Convenience wrapper around {@see Router::matchRoute()} that extracts
166+
* the path from the request URI and normalises HEAD to GET (the
167+
* HEAD method is served by the GET handler with the response payload
168+
* disabled). Prefer this over hand-rolling the parse + HEAD logic at
169+
* every call site.
170+
*/
171+
public static function matchRequest(Request $request): ?RouteMatch
172+
{
173+
$url = \parse_url($request->getURI(), PHP_URL_PATH);
174+
$url = \is_string($url) ? ($url === '' ? '/' : $url) : '/';
175+
176+
$method = $request->getMethod();
177+
$method = ($method === Http::REQUEST_METHOD_HEAD) ? Http::REQUEST_METHOD_GET : $method;
178+
179+
return self::matchRoute($method, $url);
180+
}
181+
162182
/**
163183
* @deprecated Use {@see Router::matchRoute()} which returns a per-request
164184
* {@see RouteMatch}. The old signature returned the shared Route

tests/RouterMatchRouteTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Utopia\Http;
66

77
use PHPUnit\Framework\TestCase;
8+
use Utopia\Http\Adapter\FPM\Request;
89

910
/**
1011
* Coverage for {@see Router::matchRoute()} — the coroutine-safe replacement
@@ -124,6 +125,50 @@ public function testTwoMatchesReturnDistinctRouteMatchInstances(): void
124125
$this->assertSame('/users/2', $b->urlPath);
125126
}
126127

128+
public function testMatchRequestExtractsPathFromUri(): void
129+
{
130+
$route = new Route('GET', '/users/:id');
131+
Router::addRoute($route);
132+
133+
$_SERVER['REQUEST_METHOD'] = 'GET';
134+
$_SERVER['REQUEST_URI'] = '/users/42?extra=ignored';
135+
136+
$match = Router::matchRequest(new Request());
137+
138+
$this->assertNotNull($match);
139+
$this->assertSame($route, $match->route);
140+
$this->assertSame('/users/42', $match->urlPath);
141+
}
142+
143+
public function testMatchRequestDefaultsEmptyPathToSlash(): void
144+
{
145+
$route = new Route('GET', '/');
146+
Router::addRoute($route);
147+
148+
$_SERVER['REQUEST_METHOD'] = 'GET';
149+
$_SERVER['REQUEST_URI'] = 'https://example.com?x=1';
150+
151+
$match = Router::matchRequest(new Request());
152+
153+
$this->assertNotNull($match);
154+
$this->assertSame($route, $match->route);
155+
$this->assertSame('/', $match->urlPath);
156+
}
157+
158+
public function testMatchRequestNormalisesHeadToGet(): void
159+
{
160+
$route = new Route('GET', '/head-target');
161+
Router::addRoute($route);
162+
163+
$_SERVER['REQUEST_METHOD'] = 'HEAD';
164+
$_SERVER['REQUEST_URI'] = '/head-target';
165+
166+
$match = Router::matchRequest(new Request());
167+
168+
$this->assertNotNull($match);
169+
$this->assertSame($route, $match->route, 'HEAD must resolve against the GET route.');
170+
}
171+
127172
public function testLegacyShimReturnsRoute(): void
128173
{
129174
$route = new Route('GET', '/shim');

0 commit comments

Comments
 (0)