-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* Class UriTest | ||
* @package Rayleigh\HttpMessage | ||
* @author Masaru Yamagishi <akai_inu@live.jp> | ||
* @license Apache-2.0 | ||
*/ | ||
|
||
namespace Rayleigh\HttpMessage\Tests; | ||
|
||
use PHPUnit\Framework\Attributes\CoversClass; | ||
use PHPUnit\Framework\Attributes\DataProvider; | ||
use PHPUnit\Framework\Attributes\Test; | ||
use PHPUnit\Framework\Attributes\UsesClass; | ||
use PHPUnit\Framework\TestCase; | ||
use Rayleigh\HttpMessage\HeaderBag; | ||
use Rayleigh\HttpMessage\Internal\UriPartsParser; | ||
use Rayleigh\HttpMessage\ServerRequest; | ||
use Rayleigh\HttpMessage\Uri; | ||
use Rayleigh\HttpMessage\UriPlaceholderResolver; | ||
|
||
/** | ||
* Class UriTest | ||
* @package Rayleigh\HttpMessage\Tests | ||
*/ | ||
#[CoversClass(UriPlaceholderResolver::class)] | ||
#[UsesClass(ServerRequest::class)] | ||
#[UsesClass(HeaderBag::class)] | ||
#[UsesClass(UriPartsParser::class)] | ||
#[UsesClass(Uri::class)] | ||
final class UriPlaceholderResolverTest extends TestCase | ||
{ | ||
#[Test] | ||
public function testNoPlaceholder(): void | ||
{ | ||
$resolver = new UriPlaceholderResolver(); | ||
$request = new ServerRequest('GET', '/'); | ||
|
||
$actual = $resolver->resolve($request, '/'); | ||
|
||
self::assertFalse($actual); | ||
} | ||
|
||
/** | ||
* Get resolve data | ||
* @return iterable<string, array{0: ServerRequest, 1: array<string, string|int>, 2: string}> | ||
*/ | ||
public static function getResolveData(): iterable | ||
{ | ||
return [ | ||
'Single placeholder' => [ | ||
new ServerRequest('GET', '/users/1'), | ||
['user_id' => '1'], | ||
'/users/{user_id}', | ||
], | ||
'Multiple placeholders' => [ | ||
new ServerRequest('GET', '/users/1/posts/slug'), | ||
['user_id' => '1', 'posts' => 'slug'], | ||
'/users/{user_id}/posts/{posts}', | ||
], | ||
]; | ||
} | ||
|
||
/** | ||
* Test resolve | ||
* @param ServerRequest $request | ||
* @param array<string, string|int> $expected | ||
* @param string $path | ||
* @return void | ||
*/ | ||
#[Test] | ||
#[DataProvider('getResolveData')] | ||
public function testResolve(ServerRequest $request, array $expected, string $path): void | ||
{ | ||
$resolver = new UriPlaceholderResolver(); | ||
$request = $resolver->resolve($request, $path); | ||
|
||
self::assertInstanceOf(ServerRequest::class, $request); | ||
self::assertSame($expected, $request->getAttributes()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* @author Masaru Yamagishi <akai_inu@live.jp> | ||
* @license Apache-2.0 | ||
*/ | ||
|
||
namespace Rayleigh\HttpMessage; | ||
|
||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
/* readonly */ class UriPlaceholderResolver | ||
{ | ||
public function resolve(ServerRequestInterface $request, string $path): ServerRequestInterface|bool | ||
{ | ||
$count = 0; | ||
// Replace placeholder to named capture group | ||
$regexp_pattern = \preg_replace('#\{(\w+)\}#', '(?P<$1>[^\/]+)', $path, count: $count); | ||
if ($count === 0) { | ||
return false; // no placeholder | ||
} | ||
|
||
$matches = []; | ||
$actual_path = $request->getUri()->getPath(); | ||
|
||
if (\preg_match('#^' . $regexp_pattern . '$#', $actual_path, $matches)) { | ||
/** @var array<string, string> */ | ||
$result = \array_filter($matches, fn(string|int $key): bool => !\is_int($key), \ARRAY_FILTER_USE_KEY); | ||
|
||
foreach ($result as $key => $value) { | ||
$request = $request->withAttribute($key, $value); | ||
} | ||
return $request; | ||
} | ||
|
||
return false; // @codeCoverageIgnore | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* @author Masaru Yamagishi <akai_inu@live.jp> | ||
* @license Apache-2.0 | ||
*/ | ||
|
||
namespace Rayleigh\HttpServer; | ||
|
||
use Psr\Http\Server\MiddlewareInterface; | ||
use Psr\Http\Server\RequestHandlerInterface; | ||
|
||
/** | ||
* Route information | ||
* @package Rayleigh\HttpServer | ||
*/ | ||
final /* readonly */ class Route | ||
{ | ||
/** | ||
* Constructor | ||
* @param string $method | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
*/ | ||
public function __construct( | ||
public readonly string $method, | ||
Check failure on line 29 in src/HttpServer/Route.php GitHub Actions / Unit Tests on PHP 8.2 with lowest dependenciesPossiblyUnusedProperty
Check failure on line 29 in src/HttpServer/Route.php GitHub Actions / Unit Tests on PHP 8.3 with highest dependenciesPossiblyUnusedProperty
Check failure on line 29 in src/HttpServer/Route.php GitHub Actions / Unit Tests on PHP 8.3 with lowest dependenciesPossiblyUnusedProperty
|
||
public readonly string $path, | ||
Check failure on line 30 in src/HttpServer/Route.php GitHub Actions / Unit Tests on PHP 8.2 with lowest dependenciesPossiblyUnusedProperty
Check failure on line 30 in src/HttpServer/Route.php GitHub Actions / Unit Tests on PHP 8.3 with highest dependenciesPossiblyUnusedProperty
Check failure on line 30 in src/HttpServer/Route.php GitHub Actions / Unit Tests on PHP 8.3 with lowest dependenciesPossiblyUnusedProperty
|
||
public readonly RequestHandlerInterface $handler, | ||
public readonly array $middlewares = [], | ||
) {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* @author Masaru Yamagishi <akai_inu@live.jp> | ||
* @license Apache-2.0 | ||
*/ | ||
|
||
namespace Rayleigh\HttpServer; | ||
|
||
use RuntimeException; | ||
|
||
/** | ||
* Route not found exception | ||
* @package Rayleigh\HttpServer | ||
*/ | ||
class RouteNotFoundException extends RuntimeException | ||
{ | ||
public function __construct(string $method, string $path) | ||
{ | ||
parent::__construct("Route not found: {$method} {$path}", 404); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* @author Masaru Yamagishi <akai_inu@live.jp> | ||
* @license Apache-2.0 | ||
*/ | ||
|
||
namespace Rayleigh\HttpServer; | ||
|
||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
use Psr\Http\Server\MiddlewareInterface; | ||
use Psr\Http\Server\RequestHandlerInterface; | ||
use Rayleigh\HttpMessage\UriPlaceholderResolver; | ||
|
||
/** | ||
* PSR-15 Routing handler | ||
* @package Rayleigh\HttpServer | ||
*/ | ||
final /* readonly */ class RoutingHandler implements RequestHandlerInterface | ||
{ | ||
/** @var array<string, array<string, Route>> $routes */ | ||
private array $routes = []; | ||
|
||
/** | ||
* Get routes | ||
* @return array<string, array<string, Route>> | ||
*/ | ||
public function getRoutes(): array | ||
{ | ||
return $this->routes; | ||
} | ||
|
||
/** | ||
* Add GET route | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function get(string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
$this->addRoute('GET', $path, $handler, $middlewares); | ||
} | ||
|
||
/** | ||
* Add POST route | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function post(string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
$this->addRoute('POST', $path, $handler, $middlewares); | ||
} | ||
|
||
/** | ||
* Add PUT route | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function put(string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
$this->addRoute('PUT', $path, $handler, $middlewares); | ||
} | ||
|
||
/** | ||
* Add DELETE route | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function delete(string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
$this->addRoute('DELETE', $path, $handler, $middlewares); | ||
} | ||
|
||
/** | ||
* Add PATCH route | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function patch(string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
$this->addRoute('PATCH', $path, $handler, $middlewares); | ||
} | ||
|
||
/** | ||
* Add HEAD route | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function head(string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
$this->addRoute('HEAD', $path, $handler, $middlewares); | ||
} | ||
|
||
/** | ||
* Add OPTIONS route | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function options(string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
$this->addRoute('OPTIONS', $path, $handler, $middlewares); | ||
} | ||
|
||
/** | ||
* Add TRACE route | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function trace(string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
$this->addRoute('TRACE', $path, $handler, $middlewares); | ||
} | ||
|
||
/** | ||
* Add route | ||
* @param string $method HTTP Method | ||
* @param string $path | ||
* @param RequestHandlerInterface $handler | ||
* @param array<int, MiddlewareInterface> $middlewares | ||
* @return void | ||
*/ | ||
public function addRoute(string $method, string $path, RequestHandlerInterface $handler, array $middlewares = []): void | ||
{ | ||
if (\array_key_exists($path, $this->routes) === false) { | ||
$this->routes[$path] = []; | ||
} | ||
$this->routes[$path][$method] = new Route($method, $path, $handler, $middlewares); | ||
} | ||
|
||
public function handle(ServerRequestInterface $request): ResponseInterface | ||
{ | ||
$method = \strtoupper($request->getMethod()); | ||
$path = $request->getUri()->getPath(); | ||
|
||
// Check exact routes | ||
if (\array_key_exists($path, $this->routes) && \array_key_exists($method, $this->routes[$path])) { | ||
$route = $this->routes[$path][$method]; | ||
return (new ServerRequestRunner($route->middlewares, $route->handler))->handle($request); | ||
} | ||
|
||
// Check placeholder routes | ||
$resolver = new UriPlaceholderResolver(); | ||
foreach ($this->routes as $path_pattern => $routes) { | ||
if (\array_key_exists($method, $routes) === false) { | ||
continue; | ||
} | ||
$result = $resolver->resolve($request, $path_pattern); | ||
if ($result instanceof ServerRequestInterface === false) { | ||
continue; | ||
} | ||
$route = $routes[$method]; | ||
return (new ServerRequestRunner($route->middlewares, $route->handler))->handle($result); | ||
} | ||
|
||
// Check wildcard routes | ||
if (\array_key_exists('*', $this->routes)) { | ||
$route = $this->routes['*'][$method] ?? $this->routes['*']['OPTIONS'] ?? null; | ||
if ($route !== null) { | ||
return (new ServerRequestRunner($route->middlewares, $route->handler))->handle($request); | ||
} | ||
} | ||
|
||
throw new RouteNotFoundException($method, $path); | ||
} | ||
} |