Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/Http/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/Http/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Router
Http::REQUEST_METHOD_PUT => [],
Http::REQUEST_METHOD_PATCH => [],
Http::REQUEST_METHOD_DELETE => [],
Http::REQUEST_METHOD_ANY => [],
];

/**
Expand Down Expand Up @@ -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(
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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 => [],
];
}
}
74 changes: 74 additions & 0 deletions tests/HttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
33 changes: 33 additions & 0 deletions tests/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
}
Loading