diff --git a/src/Http/Http.php b/src/Http/Http.php index c203462f..25306e3e 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -20,6 +20,8 @@ class Http public const REQUEST_METHOD_OPTIONS = 'OPTIONS'; public const REQUEST_METHOD_HEAD = 'HEAD'; + + public const REQUEST_METHOD_ANY = '*'; /** * Mode Type @@ -217,6 +219,22 @@ public static function wildcard(): Route return self::$wildcardRoute; } + + /** + * Any + * + * Add a route that will match any method but only for the specified path + * + * @param string $path + * @return Route + */ + public static function any(string $path): Route + { + $route = new Route(self::REQUEST_METHOD_ANY, $path); + Router::addRoute($route); + + return $route; + } /** * Init diff --git a/src/Http/Router.php b/src/Http/Router.php index d3324553..94814e99 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -23,6 +23,7 @@ class Router Http::REQUEST_METHOD_PUT => [], Http::REQUEST_METHOD_PATCH => [], Http::REQUEST_METHOD_DELETE => [], + Http::REQUEST_METHOD_ANY => [], ]; /** @@ -127,6 +128,7 @@ public static function match(string $method, string $path): Route|null $length = count($parts) - 1; $filteredParams = array_filter(self::$params, fn ($i) => $i <= $length); + // Try to match exact route for the specific method foreach (self::combinations($filteredParams) as $sample) { $sample = array_filter($sample, fn (int $i) => $i <= $length); $match = implode( @@ -142,6 +144,24 @@ public static function match(string $method, string $path): Route|null } } + // Try to match exact route with any method + if (array_key_exists(Http::REQUEST_METHOD_ANY, self::$routes)) { + foreach (self::combinations($filteredParams) as $sample) { + $sample = array_filter($sample, fn (int $i) => $i <= $length); + $match = implode( + '/', + array_replace( + $parts, + array_fill_keys($sample, self::PLACEHOLDER_TOKEN) + ) + ); + + if (array_key_exists($match, self::$routes[Http::REQUEST_METHOD_ANY])) { + return self::$routes[Http::REQUEST_METHOD_ANY][$match]; + } + } + } + /** * Match root wildcard. */ @@ -231,6 +251,7 @@ public static function reset(): void Http::REQUEST_METHOD_PUT => [], Http::REQUEST_METHOD_PATCH => [], Http::REQUEST_METHOD_DELETE => [], + Http::REQUEST_METHOD_ANY => [], ]; } } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 64235e0f..5d05e49c 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -587,4 +587,78 @@ public function testWildcardRoute(): void $_SERVER['REQUEST_METHOD'] = $method; $_SERVER['REQUEST_URI'] = $uri; } + + public function testAnyRoute(): void + { + Http::reset(); + + $method = $_SERVER['REQUEST_METHOD'] ?? null; + $uri = $_SERVER['REQUEST_URI'] ?? null; + + $methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; + $responseTexts = []; + + // Create a route that matches any method for a specific path + Http::any('/specific-path') + ->inject('response') + ->action(function($response) { + $response->send('ANY METHOD ROUTE'); + }); + + // Create method-specific routes for a different path + Http::get('/method-specific') + ->inject('response') + ->action(function($response) { + $response->send('GET ROUTE'); + }); + + Http::post('/method-specific') + ->inject('response') + ->action(function($response) { + $response->send('POST ROUTE'); + }); + + // Test with different methods on the 'any' route + foreach ($methods as $httpMethod) { + $_SERVER['REQUEST_METHOD'] = $httpMethod; + $_SERVER['REQUEST_URI'] = '/specific-path'; + + \ob_start(); + @$this->http->run(new Request(), new Response(), '1'); + $result = \ob_get_contents(); + \ob_end_clean(); + + $responseTexts[$httpMethod] = $result; + } + + // Check that all HTTP methods match the 'any' route + foreach ($methods as $httpMethod) { + $this->assertEquals('ANY METHOD ROUTE', $responseTexts[$httpMethod], "Method $httpMethod failed to match the 'any' route"); + } + + // Test that method-specific routes work as expected + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/method-specific'; + + \ob_start(); + @$this->http->run(new Request(), new Response(), '1'); + $result = \ob_get_contents(); + \ob_end_clean(); + + $this->assertEquals('GET ROUTE', $result, "Method-specific GET route did not match"); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['REQUEST_URI'] = '/method-specific'; + + \ob_start(); + @$this->http->run(new Request(), new Response(), '1'); + $result = \ob_get_contents(); + \ob_end_clean(); + + $this->assertEquals('POST ROUTE', $result, "Method-specific POST route did not match"); + + // Restore original server state + $_SERVER['REQUEST_METHOD'] = $method; + $_SERVER['REQUEST_URI'] = $uri; + } } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 9aed6a75..b73f3da5 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -142,4 +142,37 @@ public function testCannotFindUnknownRouteByMethod(): void $this->assertNull(Router::match(Http::REQUEST_METHOD_POST, '/404')); } + + public function testCanMatchAnyMethod(): void + { + $route = new Route(Http::REQUEST_METHOD_ANY, '/any-route'); + + Router::addRoute($route); + + // Should match any HTTP method for the specific path + $this->assertEquals($route, Router::match(Http::REQUEST_METHOD_GET, '/any-route')); + $this->assertEquals($route, Router::match(Http::REQUEST_METHOD_POST, '/any-route')); + $this->assertEquals($route, Router::match(Http::REQUEST_METHOD_PUT, '/any-route')); + $this->assertEquals($route, Router::match(Http::REQUEST_METHOD_PATCH, '/any-route')); + $this->assertEquals($route, Router::match(Http::REQUEST_METHOD_DELETE, '/any-route')); + + // But should not match other paths + $this->assertNull(Router::match(Http::REQUEST_METHOD_GET, '/different-path')); + } + + public function testMethodSpecificHasPrecedenceOverAny(): void + { + $anyRoute = new Route(Http::REQUEST_METHOD_ANY, '/test-route'); + $getRoute = new Route(Http::REQUEST_METHOD_GET, '/test-route'); + + Router::addRoute($anyRoute); + Router::addRoute($getRoute); + + // GET request should match the GET-specific route + $this->assertEquals($getRoute, Router::match(Http::REQUEST_METHOD_GET, '/test-route')); + + // Other methods should match the "any" route + $this->assertEquals($anyRoute, Router::match(Http::REQUEST_METHOD_POST, '/test-route')); + $this->assertEquals($anyRoute, Router::match(Http::REQUEST_METHOD_PUT, '/test-route')); + } }