Skip to content

Commit f3ebb16

Browse files
committed
feat: any route
1 parent ee4fc6d commit f3ebb16

4 files changed

Lines changed: 146 additions & 0 deletions

File tree

src/Http/Http.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class Http
2020
public const REQUEST_METHOD_OPTIONS = 'OPTIONS';
2121

2222
public const REQUEST_METHOD_HEAD = 'HEAD';
23+
24+
public const REQUEST_METHOD_ANY = '*';
2325

2426
/**
2527
* Mode Type
@@ -217,6 +219,22 @@ public static function wildcard(): Route
217219

218220
return self::$wildcardRoute;
219221
}
222+
223+
/**
224+
* Any
225+
*
226+
* Add a route that will match any method but only for the specified path
227+
*
228+
* @param string $path
229+
* @return Route
230+
*/
231+
public static function any(string $path): Route
232+
{
233+
$route = new Route(self::REQUEST_METHOD_ANY, $path);
234+
Router::addRoute($route);
235+
236+
return $route;
237+
}
220238

221239
/**
222240
* Init

src/Http/Router.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Router
2323
Http::REQUEST_METHOD_PUT => [],
2424
Http::REQUEST_METHOD_PATCH => [],
2525
Http::REQUEST_METHOD_DELETE => [],
26+
Http::REQUEST_METHOD_ANY => [],
2627
];
2728

2829
/**
@@ -127,6 +128,7 @@ public static function match(string $method, string $path): Route|null
127128
$length = count($parts) - 1;
128129
$filteredParams = array_filter(self::$params, fn ($i) => $i <= $length);
129130

131+
// Try to match exact route for the specific method
130132
foreach (self::combinations($filteredParams) as $sample) {
131133
$sample = array_filter($sample, fn (int $i) => $i <= $length);
132134
$match = implode(
@@ -142,6 +144,24 @@ public static function match(string $method, string $path): Route|null
142144
}
143145
}
144146

147+
// Try to match exact route with any method
148+
if (array_key_exists(Http::REQUEST_METHOD_ANY, self::$routes)) {
149+
foreach (self::combinations($filteredParams) as $sample) {
150+
$sample = array_filter($sample, fn (int $i) => $i <= $length);
151+
$match = implode(
152+
'/',
153+
array_replace(
154+
$parts,
155+
array_fill_keys($sample, self::PLACEHOLDER_TOKEN)
156+
)
157+
);
158+
159+
if (array_key_exists($match, self::$routes[Http::REQUEST_METHOD_ANY])) {
160+
return self::$routes[Http::REQUEST_METHOD_ANY][$match];
161+
}
162+
}
163+
}
164+
145165
/**
146166
* Match root wildcard.
147167
*/
@@ -231,6 +251,7 @@ public static function reset(): void
231251
Http::REQUEST_METHOD_PUT => [],
232252
Http::REQUEST_METHOD_PATCH => [],
233253
Http::REQUEST_METHOD_DELETE => [],
254+
Http::REQUEST_METHOD_ANY => [],
234255
];
235256
}
236257
}

tests/HttpTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,4 +587,78 @@ public function testWildcardRoute(): void
587587
$_SERVER['REQUEST_METHOD'] = $method;
588588
$_SERVER['REQUEST_URI'] = $uri;
589589
}
590+
591+
public function testAnyRoute(): void
592+
{
593+
Http::reset();
594+
595+
$method = $_SERVER['REQUEST_METHOD'] ?? null;
596+
$uri = $_SERVER['REQUEST_URI'] ?? null;
597+
598+
$methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
599+
$responseTexts = [];
600+
601+
// Create a route that matches any method for a specific path
602+
Http::any('/specific-path')
603+
->inject('response')
604+
->action(function($response) {
605+
$response->send('ANY METHOD ROUTE');
606+
});
607+
608+
// Create method-specific routes for a different path
609+
Http::get('/method-specific')
610+
->inject('response')
611+
->action(function($response) {
612+
$response->send('GET ROUTE');
613+
});
614+
615+
Http::post('/method-specific')
616+
->inject('response')
617+
->action(function($response) {
618+
$response->send('POST ROUTE');
619+
});
620+
621+
// Test with different methods on the 'any' route
622+
foreach ($methods as $httpMethod) {
623+
$_SERVER['REQUEST_METHOD'] = $httpMethod;
624+
$_SERVER['REQUEST_URI'] = '/specific-path';
625+
626+
\ob_start();
627+
@$this->http->run(new Request(), new Response(), '1');
628+
$result = \ob_get_contents();
629+
\ob_end_clean();
630+
631+
$responseTexts[$httpMethod] = $result;
632+
}
633+
634+
// Check that all HTTP methods match the 'any' route
635+
foreach ($methods as $httpMethod) {
636+
$this->assertEquals('ANY METHOD ROUTE', $responseTexts[$httpMethod], "Method $httpMethod failed to match the 'any' route");
637+
}
638+
639+
// Test that method-specific routes work as expected
640+
$_SERVER['REQUEST_METHOD'] = 'GET';
641+
$_SERVER['REQUEST_URI'] = '/method-specific';
642+
643+
\ob_start();
644+
@$this->http->run(new Request(), new Response(), '1');
645+
$result = \ob_get_contents();
646+
\ob_end_clean();
647+
648+
$this->assertEquals('GET ROUTE', $result, "Method-specific GET route did not match");
649+
650+
$_SERVER['REQUEST_METHOD'] = 'POST';
651+
$_SERVER['REQUEST_URI'] = '/method-specific';
652+
653+
\ob_start();
654+
@$this->http->run(new Request(), new Response(), '1');
655+
$result = \ob_get_contents();
656+
\ob_end_clean();
657+
658+
$this->assertEquals('POST ROUTE', $result, "Method-specific POST route did not match");
659+
660+
// Restore original server state
661+
$_SERVER['REQUEST_METHOD'] = $method;
662+
$_SERVER['REQUEST_URI'] = $uri;
663+
}
590664
}

tests/RouterTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,37 @@ public function testCannotFindUnknownRouteByMethod(): void
142142

143143
$this->assertNull(Router::match(Http::REQUEST_METHOD_POST, '/404'));
144144
}
145+
146+
public function testCanMatchAnyMethod(): void
147+
{
148+
$route = new Route(Http::REQUEST_METHOD_ANY, '/any-route');
149+
150+
Router::addRoute($route);
151+
152+
// Should match any HTTP method for the specific path
153+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_GET, '/any-route'));
154+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_POST, '/any-route'));
155+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_PUT, '/any-route'));
156+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_PATCH, '/any-route'));
157+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_DELETE, '/any-route'));
158+
159+
// But should not match other paths
160+
$this->assertNull(Router::match(Http::REQUEST_METHOD_GET, '/different-path'));
161+
}
162+
163+
public function testMethodSpecificHasPrecedenceOverAny(): void
164+
{
165+
$anyRoute = new Route(Http::REQUEST_METHOD_ANY, '/test-route');
166+
$getRoute = new Route(Http::REQUEST_METHOD_GET, '/test-route');
167+
168+
Router::addRoute($anyRoute);
169+
Router::addRoute($getRoute);
170+
171+
// GET request should match the GET-specific route
172+
$this->assertEquals($getRoute, Router::match(Http::REQUEST_METHOD_GET, '/test-route'));
173+
174+
// Other methods should match the "any" route
175+
$this->assertEquals($anyRoute, Router::match(Http::REQUEST_METHOD_POST, '/test-route'));
176+
$this->assertEquals($anyRoute, Router::match(Http::REQUEST_METHOD_PUT, '/test-route'));
177+
}
145178
}

0 commit comments

Comments
 (0)