Account for trailing slashes in URLs.

This commit is contained in:
flash 2025-03-23 03:45:47 +00:00
parent 2bb9cccf18
commit 2372a113d2
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
5 changed files with 20 additions and 9 deletions

View file

@ -1 +1 @@
0.2503.230337 0.2503.230355

View file

@ -1,7 +1,7 @@
<?php <?php
// PatternFilter.php // PatternFilter.php
// Created: 2024-03-28 // Created: 2024-03-28
// Updated: 2025-03-07 // Updated: 2025-03-23
namespace Index\Http\Routing\Filters; namespace Index\Http\Routing\Filters;
@ -18,14 +18,14 @@ class PatternFilter extends FilterAttribute {
/** /**
* @param string $pattern URI path pattern. * @param string $pattern URI path pattern.
* @param bool $raw If true, $pattern is taken as is, if false, $pattern is prefixed with #^ and suffixed with #u or $#uD * @param bool $raw If true, $pattern is taken as is, if false, $pattern is prefixed with #^ and suffixed with #u or $#uD
* @param bool $prefix Only has effect if $raw is false. If true is #u appended to $pattern, otherwise $#uD. * @param bool $prefix Only has effect if $raw is false. If true is #u appended to $pattern, otherwise /?$#uD.
*/ */
public function __construct( public function __construct(
string $pattern, string $pattern,
bool $raw = false, bool $raw = false,
bool $prefix = true, bool $prefix = true,
) { ) {
$this->pattern = $raw ? $pattern : sprintf('#^%s%s', $pattern, $prefix ? '#u' : '$#uD'); $this->pattern = $raw ? $pattern : sprintf('#^%s%s', $pattern, $prefix ? '#u' : '/?$#uD');
} }
public function createInstance(Closure $handler): FilterInfo { public function createInstance(Closure $handler): FilterInfo {

View file

@ -1,7 +1,7 @@
<?php <?php
// PatternRoute.php // PatternRoute.php
// Created: 2024-03-28 // Created: 2024-03-28
// Updated: 2025-03-07 // Updated: 2025-03-23
namespace Index\Http\Routing\Routes; namespace Index\Http\Routing\Routes;
@ -19,13 +19,15 @@ class PatternRoute extends RouteAttribute {
* @param string $method HTTP Method. * @param string $method HTTP Method.
* @param string $pattern URI path pattern. * @param string $pattern URI path pattern.
* @param bool $raw If true, $pattern is taken as is, if false, $pattern is prefixed with #^ and suffixed with $#uD * @param bool $raw If true, $pattern is taken as is, if false, $pattern is prefixed with #^ and suffixed with $#uD
* @param bool $slash Only has effect if $raw is false. If true /? is appended to $pattern to allow for trailing slashes.
*/ */
public function __construct( public function __construct(
private string $method, private string $method,
string $pattern, string $pattern,
bool $raw = false bool $raw = false,
bool $slash = true,
) { ) {
$this->pattern = $raw ? $pattern : sprintf('#^%s$#uD', $pattern); $this->pattern = $raw ? $pattern : sprintf('#^%s%s$#uD', $pattern, $slash ? '/?' : '');
} }
public function createInstance(Closure $handler): RouteInfo { public function createInstance(Closure $handler): RouteInfo {

View file

@ -1,7 +1,7 @@
<?php <?php
// ExactPathUriMatcher.php // ExactPathUriMatcher.php
// Created: 2025-03-07 // Created: 2025-03-07
// Updated: 2025-03-07 // Updated: 2025-03-23
namespace Index\Http\Routing\UriMatchers; namespace Index\Http\Routing\UriMatchers;
@ -19,6 +19,7 @@ class ExactPathUriMatcher implements UriMatcher {
) {} ) {}
public function match(UriInterface $uri): array|bool { public function match(UriInterface $uri): array|bool {
return $uri->getPath() === $this->path; $path = $uri->getPath();
return $path === $this->path || $path === ($this->path . '/');
} }
} }

View file

@ -139,6 +139,12 @@ final class RouterTest extends TestCase {
return 'hit pattern'; return 'hit pattern';
} }
#[ExactRoute('GET', '/assets/avatar')]
#[PatternRoute('GET', '/assets/avatar/([0-9]+)(?:\.[a-z]+)?')]
public function getTrailingSlash(): string {
return 'hit';
}
public function hasNoAttr(): string { public function hasNoAttr(): string {
return 'not a route'; return 'not a route';
} }
@ -163,6 +169,8 @@ final class RouterTest extends TestCase {
$this->assertSame('1RJNSRYmxrvXUr////jpg', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr.jpg'))->getBody()); $this->assertSame('1RJNSRYmxrvXUr////jpg', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr.jpg'))->getBody());
$this->assertSame('hit exact', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/messages/stats'))->getBody()); $this->assertSame('hit exact', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/messages/stats'))->getBody());
$this->assertSame('hit pattern', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/messages/soaps'))->getBody()); $this->assertSame('hit pattern', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/messages/soaps'))->getBody());
$this->assertSame('hit', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/assets/avatar/'))->getBody());
$this->assertSame('hit', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/assets/avatar/123/'))->getBody());
} }
public function testEEPROMSituation(): void { public function testEEPROMSituation(): void {