Skip to content
Merged
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
33 changes: 4 additions & 29 deletions Slim/Routing/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,17 @@ public function map(array $methods, string $path, callable|string $handler): Rou
throw new InvalidArgumentException('HTTP methods array cannot be empty');
}

$routePattern = $this->normalizePath($path);
$route = new Route($methods, $routePattern, $handler, null);
$route = new Route($methods, $path, $handler, null);

$this->collector->addRoute($methods, $routePattern, $route);
$this->collector->addRoute($methods, $path, $route);

return $route;
}

public function group(string $path, callable $handler): RouteGroup
{
$routePattern = $this->normalizePath($path);
$routeGroup = new RouteGroup($routePattern, $handler, $this->getRouteCollector());
$this->collector->addGroup($routePattern, $routeGroup);
$routeGroup = new RouteGroup($path, $handler, $this->getRouteCollector());
$this->collector->addGroup($path, $routeGroup);

return $routeGroup;
}
Expand All @@ -80,27 +78,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface
->withPipeline($this->getMiddleware())
->handle($request);
}

/**
* Normalizes a path by ensuring:
* - Starts with a forward slash
* - No trailing slash (unless root path)
* - No double slashes
*/
private function normalizePath(string $path): string
{
// If path is empty or just a slash, return single slash
if ($path === '' || $path === '/') {
return '/';
}

// Ensure path starts with a slash
$path = '/' . ltrim($path, '/');

// Remove trailing slash unless it's the root path
$path = rtrim($path, '/');

// Replace multiple consecutive slashes with a single slash
return preg_replace('#/+#', '/', $path) ?? '';
}
}
55 changes: 48 additions & 7 deletions tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,20 +277,20 @@ public static function routePatternsProvider(): array
{
return [
// Route pattern -> http uri
// Empty route
['', '/'],
// Single slash route
['/', '/'],
// Route That Does Not Start With A Slash
['foo', '/foo'],
// Route That Does Not End In A Slash
['/foo', '/foo'],
// Route That Ends In A Slash
['/foo/', '/foo'],
['/foo/', '/foo/'],
// Route That Ends In A double Slash
['/foo//', '/foo'],
['/foo//', '/foo//'],
// Route That contains In A double Slash
['/foo//bar', '/foo/bar'],
['/foo//bar', '/foo//bar'],
// FastRoute optional trailing slash segment matches /foo
['/foo[/]', '/foo'],
// FastRoute optional trailing slash segment matches /foo/
['/foo[/]', '/foo/'],
];
}

Expand All @@ -315,6 +315,47 @@ public function testRoutePatterns(string $pattern, string $uri): void
$this->assertSame('Hello World', (string)$response->getBody());
}

public static function strictRouteMismatchProvider(): array
{
return [
'foo does not match foo trailing slash' => [
'/foo', // route pattern
'/foo/', // request URI
],
'foo trailing slash does not match foo' => [
'/foo/', // route pattern
'/foo', // request URI
],
'foo does not match FOO uppercase' => [
'/foo', // route pattern
'/FOO', // request URI
],
'route without leading slash does not match' => [
'foo', // route pattern
'/foo', // request URI
],
];
}

#[DataProvider('strictRouteMismatchProvider')]
public function testStrictRouteMismatch(string $routePattern, string $requestUri): void
{
$this->expectException(HttpNotFoundException::class);

$app = AppFactory::create();
$app->addRoutingMiddleware();

$request = $this
->getServerRequestFactory($app)
->createServerRequest('GET', $requestUri);

$app->get($routePattern, function () {
// noop
});

$app->handle($request);
}

/********************************************************************************
* Route Groups
*******************************************************************************/
Expand Down