From 6c78d8c8f8aede7c18b03b78f8c9545349e1ac31 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Sun, 31 Aug 2025 20:38:17 +0700 Subject: [PATCH 01/17] add options to control method failure handling --- CHANGELOG.md | 2 + README.md | 178 ++++++++++++++++++++++++++++---- src/Middleware/Router.php | 55 ++++++++-- tests/Middleware/RouterTest.php | 73 ++++++++++++- 4 files changed, 276 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3d012d..ad13ae49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 4.0.1 under development - Enh #265: Adapt summary data in debug collector (@rustamwin) +- New #249: Add option to ignore method failure handler to the 'Router' middleware (@olegbaturin) +- New #249: Add custom response handlers for the method failure responses to the 'Router' middleware (@olegbaturin) ## 4.0.0 February 25, 2025 diff --git a/README.md b/README.md index 54c7de03..5583682c 100644 --- a/README.md +++ b/README.md @@ -106,22 +106,6 @@ $response = $result->process($request, $notFoundHandler); > to specific adapter documentation. All examples in this document are for > [FastRoute adapter](https://github.com/yiisoft/router-fastroute). -### Middleware usage - -In order to simplify usage in PSR-middleware based application, there is a ready to use middleware provided: - -```php -$router = $container->get(Yiisoft\Router\UrlMatcherInterface::class); -$responseFactory = $container->get(\Psr\Http\Message\ResponseFactoryInterface::class); - -$routerMiddleware = new Yiisoft\Router\Middleware\Router($router, $responseFactory, $container); - -// Add middleware to your middleware handler of choice. -``` - -In case of a route match router middleware executes handler middleware attached to the route. If there is no match, next -application middleware processes the request. - ### Routes Route could match for one or more HTTP methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`. There are @@ -233,17 +217,171 @@ and `disableMiddleware()`. These middleware are executed prior to matched route' If host is specified, all routes in the group would match only if the host match. -### Automatic OPTIONS response and CORS +### Middleware usage + +To simplify usage in PSR-middleware based application, there is a ready to use `Yiisoft\Router\Middleware\Router` middleware provided: + +```php +$router = $container->get(Yiisoft\Router\UrlMatcherInterface::class); +$responseFactory = $container->get(\Psr\Http\Message\ResponseFactoryInterface::class); + +$routerMiddleware = new Yiisoft\Router\Middleware\Router($router, $responseFactory, $container); + +// Add middleware to your middleware handler of choice. +``` + +When a route matches router middleware executes handler middleware attached to the route. If there is no match, next +application middleware processes the request. + +### Automatic responses + +`Yiisoft\Router\Middleware\Router` middleware responds automatically to: +- `OPTIONS` requests, see [OPTIONS requests](#options-requests) section below. +- Requests with methods that are not supported by the target resource, see [Method not allowed response](#method-not-allowed-response) section below. + +You can disable this behavior by calling the `Yiisoft\Router\Middleware\Router::ignoreMethodFailureHandler()` method: + +```php +use Yiisoft\Router\Middleware\Router; -By default, router responds automatically to OPTIONS requests based on the routes defined: +$routerMiddleware = new Router($router, $responseFactory, $middlewareFactory, $currentRoute); + +// Returns a new instance with the turned off method failure error handler. +$routerMiddleware = $routerMiddleware->ignoreMethodFailureHandler(); +``` + +or define the `Yiisoft\Router\Middleware\Router` configuration in the DI container: + +`config/common/di/router.php` + +```php +use Yiisoft\Router\Middleware\Router; + +return [ + Router::class => [ + 'ignoreMethodFailureHandler()' => [], + ], +]; +``` + +#### OPTIONS requests + +By default, `Yiisoft\Router\Middleware\Router` middleware responds to `OPTIONS` requests based on the routes defined: ``` HTTP/1.1 204 No Content Allow: GET, HEAD ``` -Generally that is fine unless you need [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). In this -case, you can add a middleware for handling it such as [tuupola/cors-middleware](https://github.com/tuupola/cors-middleware): +You can change this behavior by implementing your own request handler: + +```php +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Yiisoft\Http\Header; +use Yiisoft\Http\Status; +use Yiisoft\Router\MethodFailureHandlerInterface; +use Yiisoft\Router\Middleware\Router; + +$allowedMethodsHandler = new class implements MethodFailureHandlerInterface { + private array $methods; + + public function handle(ServerRequestInterface $request): ResponseInterface + { + return (new Response(Status::OK)) + ->withHeader(Header::ALLOW, implode(', ', $this->methods)); + } + + public function withAllowedMethods(array $methods): self + { + $new = clone $this; + $new->methods = $methods; + return $new; + } + }; + +$routerMiddleware = new Router( + $router, + $responseFactory, + $middlewareFactory, + $currentRoute, + allowedMethodsHandler: $allowedMethodsHandler +); +``` + +or define the `Yiisoft\Router\Middleware\Router` configuration in the DI container: + +`config/common/di/router.php` + +```php +use Yiisoft\Definitions\Reference; +use Yiisoft\Router\Middleware\Router; +use App\AllowedMethodsHandler; + +return [ + Router::class => [ + '__construct()' => [ + 'allowedMethodsHandler' => Reference::to(AllowedMethodsHandler::class), + ], + ], +]; +``` + +#### `Method not allowed` response + +By default, `Yiisoft\Router\Middleware\Router` middleware responds to requests with methods that are not supported by the target resource based on the routes defined: + +``` +HTTP/1.1 405 Method Not Allowed +Allow: GET, HEAD +``` + +You can change this behavior by implementing your own request handler: + +```php +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Yiisoft\Http\Status; +use Yiisoft\Router\MethodFailureHandlerInterface; +use Yiisoft\Router\Middleware\Router; + +$methodNotAllowedHandler = new class implements MethodFailureHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + return (new Response(Status::BAD_REQUEST)); + } + }; + +$routerMiddleware = new Router( + $router, + $responseFactory, + $middlewareFactory, + $currentRoute, + methodNotAllowedHandler: $methodNotAllowedHandler +); +``` + +or define the `Yiisoft\Router\Middleware\Router` configuration in the DI container: + +`config/common/di/router.php` + +```php +use Yiisoft\Definitions\Reference; +use Yiisoft\Router\Middleware\Router; +use App\MethodNotAllowedHandler; + +return [ + Router::class => [ + '__construct()' => [ + 'methodNotAllowedHandler' => Reference::to(MethodNotAllowedHandler::class), + ], + ], +]; +``` + +### CORS protocol + +If you need [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) you can add a middleware for handling it such as [tuupola/cors-middleware](https://github.com/tuupola/cors-middleware): ```php use Yiisoft\Router\Group; diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index 78d130cb..09d0fcdd 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -10,23 +10,28 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Yiisoft\Http\Header; use Yiisoft\Http\Method; use Yiisoft\Http\Status; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\CurrentRoute; +use Yiisoft\Router\MethodFailureHandlerInterface; use Yiisoft\Router\UrlMatcherInterface; final class Router implements MiddlewareInterface { private readonly MiddlewareDispatcher $dispatcher; + private bool $ignoreMethodFailureHandler = false; public function __construct( private readonly UrlMatcherInterface $matcher, private readonly ResponseFactoryInterface $responseFactory, MiddlewareFactory $middlewareFactory, private readonly CurrentRoute $currentRoute, - ?EventDispatcherInterface $eventDispatcher = null + ?EventDispatcherInterface $eventDispatcher = null, + private readonly ?MethodFailureHandlerInterface $allowedMethodsHandler = null, + private readonly ?MethodFailureHandlerInterface $methodNotAllowedHandler = null ) { $this->dispatcher = new MiddlewareDispatcher($middlewareFactory, $eventDispatcher); } @@ -37,15 +42,10 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setUri($request->getUri()); - if ($result->isMethodFailure()) { - if ($request->getMethod() === Method::OPTIONS) { - return $this->responseFactory - ->createResponse(Status::NO_CONTENT) - ->withHeader('Allow', implode(', ', $result->methods())); - } - return $this->responseFactory - ->createResponse(Status::METHOD_NOT_ALLOWED) - ->withHeader('Allow', implode(', ', $result->methods())); + if (!$this->ignoreMethodFailureHandler && $result->isMethodFailure()) { + return $request->getMethod() === Method::OPTIONS + ? $this->getAllowedMethodsResponse($request, $result->methods()) + : $this->getMethodNotAllowedResponse($request, $result->methods()); } if (!$result->isSuccess()) { @@ -58,4 +58,39 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface ->withMiddlewares($result->route()->getData('enabledMiddlewares')) ->dispatch($request, $handler); } + + public function ignoreMethodFailureHandler(): self + { + $new = clone $this; + $new->ignoreMethodFailureHandler = true; + return $new; + } + + /** + * @param string[] $methods + */ + private function getAllowedMethodsResponse(ServerRequestInterface $request, array $methods): ResponseInterface + { + return $this->allowedMethodsHandler !== null + ? $this->allowedMethodsHandler + ->withAllowedMethods($methods) + ->handle($request) + : $this->responseFactory + ->createResponse(Status::NO_CONTENT) + ->withHeader(Header::ALLOW, implode(', ', $methods)); + } + + /** + * @param string[] $methods + */ + private function getMethodNotAllowedResponse(ServerRequestInterface $request, array $methods): ResponseInterface + { + return $this->methodNotAllowedHandler !== null + ? $this->methodNotAllowedHandler + ->withAllowedMethods($methods) + ->handle($request) + : $this->responseFactory + ->createResponse(Status::METHOD_NOT_ALLOWED) + ->withHeader(Header::ALLOW, implode(', ', $methods)); + } } diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index 2ec3efa2..abac788a 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -18,6 +18,7 @@ use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Group; use Yiisoft\Router\MatchingResult; +use Yiisoft\Router\MethodFailureHandlerInterface; use Yiisoft\Router\Middleware\Router; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollection; @@ -51,6 +52,17 @@ public function testMethodMismatchRespondWith405(): void $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); } + public function testMethodMismatchHandlerRespondWith400(): void + { + $request = new ServerRequest('POST', '/'); + $response = $this + ->createRouterMiddleware(methodNotAllowedHandler: $this->creatMethodFailureHandler(400)) + ->process($request, $this->createRequestHandler()); + $this->assertSame(400, $response->getStatusCode()); + $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); + $this->assertSame('JGURDA', $response->getHeaderLine('X-JGURDA')); + } + public function testAutoResponseOptions(): void { $request = new ServerRequest('OPTIONS', '/'); @@ -59,6 +71,27 @@ public function testAutoResponseOptions(): void $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); } + public function testAutoResponseOptionsHandler(): void + { + $request = new ServerRequest('OPTIONS', '/'); + $response = $this + ->createRouterMiddleware(allowedMethodsHandler: $this->creatMethodFailureHandler(200)) + ->process($request, $this->createRequestHandler()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); + $this->assertSame('JGURDA', $response->getHeaderLine('X-JGURDA')); + } + + public function testIgnoreMethodFailureHandlerRespondWith404(): void + { + $request = new ServerRequest('POST', '/'); + $response = $this + ->createRouterMiddleware() + ->ignoreMethodFailureHandler() + ->process($request, $this->createRequestHandler()); + $this->assertSame(404, $response->getStatusCode()); + } + public function testAutoResponseOptionsWithOrigin(): void { $request = new ServerRequest('OPTIONS', 'http://test.local/', ['Origin' => 'http://test.com']); @@ -205,6 +238,13 @@ public function testRouteMiddleware(int $expectedCode, mixed $middleware): void $this->assertSame($expectedCode, $response->getStatusCode()); } + public function testImmutability(): void + { + $original = $this->createRouterMiddleware(); + + $this->assertNotSame($original, $original->ignoreMethodFailureHandler()); + } + private function getMatcher(?RouteCollectionInterface $routeCollection = null): UrlMatcherInterface { $middleware = $this->createRouteMiddleware(); @@ -265,6 +305,8 @@ public function createResponse(int $code = 200, string $reasonPhrase = ''): Resp private function createRouterMiddleware( ?RouteCollectionInterface $routeCollection = null, ?CurrentRoute $currentRoute = null, + ?MethodFailureHandlerInterface $allowedMethodsHandler = null, + ?MethodFailureHandlerInterface $methodNotAllowedHandler = null, array $containerDefinitions = [], ): Router { $container = new SimpleContainer( @@ -278,7 +320,10 @@ private function createRouterMiddleware( $this->getMatcher($routeCollection), new Psr17Factory(), new MiddlewareFactory($container), - $currentRoute ?? new CurrentRoute() + $currentRoute ?? new CurrentRoute(), + null, + $allowedMethodsHandler, + $methodNotAllowedHandler, ); } @@ -289,7 +334,7 @@ private function processWithRouter( array $containerDefinitions = [], ): ResponseInterface { return $this - ->createRouterMiddleware($routes, $currentRoute, $containerDefinitions) + ->createRouterMiddleware($routes, $currentRoute, containerDefinitions: $containerDefinitions) ->process($request, $this->createRequestHandler()); } @@ -307,4 +352,28 @@ private function createRouteMiddleware(): callable { return static fn () => new Response(201); } + + private function creatMethodFailureHandler(int $code): MethodFailureHandlerInterface + { + return new class ($code) implements MethodFailureHandlerInterface { + private array $methods; + + public function __construct(private int $code) + {} + + public function handle(ServerRequestInterface $request): ResponseInterface + { + return (new Response($this->code)) + ->withHeader('Allow', implode(', ', $this->methods)) + ->withHeader('X-JGURDA', 'JGURDA'); + } + + public function withAllowedMethods(array $methods): self + { + $new = clone $this; + $new->methods = $methods; + return $new; + } + }; + } } From 085be6f0effd60eb6fc125c70ef15180bacae067 Mon Sep 17 00:00:00 2001 From: olegbaturin <15981018+olegbaturin@users.noreply.github.com> Date: Sun, 31 Aug 2025 13:41:27 +0000 Subject: [PATCH 02/17] Apply Rector changes (CI) --- src/Debug/RouterCollector.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Debug/RouterCollector.php b/src/Debug/RouterCollector.php index 8ff2b780..89edeb6e 100644 --- a/src/Debug/RouterCollector.php +++ b/src/Debug/RouterCollector.php @@ -117,7 +117,6 @@ private function getRouteByCurrentRoute(?CurrentRoute $currentRoute): ?Route $reflection = new ReflectionObject($currentRoute); $reflectionProperty = $reflection->getProperty('route'); - $reflectionProperty->setAccessible(true); /** * @var Route $value From b6715daeec1a0124728fc04c4eb3c48a26ce2b6f Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Sun, 31 Aug 2025 20:56:02 +0700 Subject: [PATCH 03/17] add MethodFailureHandlerInterface --- src/MethodFailureHandlerInterface.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/MethodFailureHandlerInterface.php diff --git a/src/MethodFailureHandlerInterface.php b/src/MethodFailureHandlerInterface.php new file mode 100644 index 00000000..fb1dd03f --- /dev/null +++ b/src/MethodFailureHandlerInterface.php @@ -0,0 +1,20 @@ + Date: Mon, 1 Sep 2025 20:57:33 +0700 Subject: [PATCH 04/17] move default responses to the default classes --- src/AllowedMethodsHandler.php | 33 ++++++++++++++++++++++ src/MethodNotAllowedHandler.php | 33 ++++++++++++++++++++++ src/Middleware/Router.php | 50 ++++++++++----------------------- 3 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/AllowedMethodsHandler.php create mode 100644 src/MethodNotAllowedHandler.php diff --git a/src/AllowedMethodsHandler.php b/src/AllowedMethodsHandler.php new file mode 100644 index 00000000..d0bbfb49 --- /dev/null +++ b/src/AllowedMethodsHandler.php @@ -0,0 +1,33 @@ +responseFactory + ->createResponse(Status::NO_CONTENT) + ->withHeader(Header::ALLOW, implode(', ', $this->methods)); + } + + public function withAllowedMethods(array $methods): self + { + $new = clone $this; + $new->methods = $methods; + return $new; + } +} diff --git a/src/MethodNotAllowedHandler.php b/src/MethodNotAllowedHandler.php new file mode 100644 index 00000000..473863f1 --- /dev/null +++ b/src/MethodNotAllowedHandler.php @@ -0,0 +1,33 @@ +responseFactory + ->createResponse(Status::METHOD_NOT_ALLOWED) + ->withHeader(Header::ALLOW, implode(', ', $this->methods)); + } + + public function withAllowedMethods(array $methods): self + { + $new = clone $this; + $new->methods = $methods; + return $new; + } +} diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index 09d0fcdd..a6450fe3 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -10,30 +10,34 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Yiisoft\Http\Header; use Yiisoft\Http\Method; -use Yiisoft\Http\Status; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\CurrentRoute; +use Yiisoft\Router\AllowedMethodsHandler; use Yiisoft\Router\MethodFailureHandlerInterface; +use Yiisoft\Router\MethodNotAllowedHandler; use Yiisoft\Router\UrlMatcherInterface; final class Router implements MiddlewareInterface { private readonly MiddlewareDispatcher $dispatcher; + private readonly ?MethodFailureHandlerInterface $allowedMethodsHandler; + private readonly ?MethodFailureHandlerInterface $methodNotAllowedHandler; private bool $ignoreMethodFailureHandler = false; public function __construct( private readonly UrlMatcherInterface $matcher, - private readonly ResponseFactoryInterface $responseFactory, + ResponseFactoryInterface $responseFactory, MiddlewareFactory $middlewareFactory, private readonly CurrentRoute $currentRoute, ?EventDispatcherInterface $eventDispatcher = null, - private readonly ?MethodFailureHandlerInterface $allowedMethodsHandler = null, - private readonly ?MethodFailureHandlerInterface $methodNotAllowedHandler = null + ?MethodFailureHandlerInterface $allowedMethodsHandler = null, + ?MethodFailureHandlerInterface $methodNotAllowedHandler = null ) { $this->dispatcher = new MiddlewareDispatcher($middlewareFactory, $eventDispatcher); + $this->allowedMethodsHandler = $allowedMethodsHandler ?? new AllowedMethodsHandler($responseFactory); + $this->methodNotAllowedHandler = $methodNotAllowedHandler ?? new MethodNotAllowedHandler($responseFactory); } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface @@ -44,8 +48,12 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if (!$this->ignoreMethodFailureHandler && $result->isMethodFailure()) { return $request->getMethod() === Method::OPTIONS - ? $this->getAllowedMethodsResponse($request, $result->methods()) - : $this->getMethodNotAllowedResponse($request, $result->methods()); + ? $this->allowedMethodsHandler + ->withAllowedMethods($result->methods()) + ->handle($request) + : $this->methodNotAllowedHandler + ->withAllowedMethods($result->methods()) + ->handle($request); } if (!$result->isSuccess()) { @@ -65,32 +73,4 @@ public function ignoreMethodFailureHandler(): self $new->ignoreMethodFailureHandler = true; return $new; } - - /** - * @param string[] $methods - */ - private function getAllowedMethodsResponse(ServerRequestInterface $request, array $methods): ResponseInterface - { - return $this->allowedMethodsHandler !== null - ? $this->allowedMethodsHandler - ->withAllowedMethods($methods) - ->handle($request) - : $this->responseFactory - ->createResponse(Status::NO_CONTENT) - ->withHeader(Header::ALLOW, implode(', ', $methods)); - } - - /** - * @param string[] $methods - */ - private function getMethodNotAllowedResponse(ServerRequestInterface $request, array $methods): ResponseInterface - { - return $this->methodNotAllowedHandler !== null - ? $this->methodNotAllowedHandler - ->withAllowedMethods($methods) - ->handle($request) - : $this->responseFactory - ->createResponse(Status::METHOD_NOT_ALLOWED) - ->withHeader(Header::ALLOW, implode(', ', $methods)); - } } From 89061994ccccc842b3f1e14387eccd8d32ff9530 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Tue, 2 Sep 2025 18:40:37 +0700 Subject: [PATCH 05/17] add tests for handlers --- src/AllowedMethodsHandler.php | 8 ++++ src/MethodNotAllowedHandler.php | 8 ++++ .../Middleware/AllowedMethodsHandlerTest.php | 48 +++++++++++++++++++ .../MethodNotAllowedHandlerTest.php | 48 +++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 tests/Middleware/AllowedMethodsHandlerTest.php create mode 100644 tests/Middleware/MethodNotAllowedHandlerTest.php diff --git a/src/AllowedMethodsHandler.php b/src/AllowedMethodsHandler.php index d0bbfb49..1894baa4 100644 --- a/src/AllowedMethodsHandler.php +++ b/src/AllowedMethodsHandler.php @@ -4,6 +4,7 @@ namespace Yiisoft\Router; +use InvalidArgumentException; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -17,8 +18,15 @@ class AllowedMethodsHandler implements MethodFailureHandlerInterface public function __construct(private readonly ResponseFactoryInterface $responseFactory) { } + /** + * @throws InvalidArgumentException when methods are empty or not set + */ public function handle(ServerRequestInterface $request): ResponseInterface { + if (empty($this->methods)) { + throw new InvalidArgumentException("Allowed methods can't be empty array."); + } + return $this->responseFactory ->createResponse(Status::NO_CONTENT) ->withHeader(Header::ALLOW, implode(', ', $this->methods)); diff --git a/src/MethodNotAllowedHandler.php b/src/MethodNotAllowedHandler.php index 473863f1..94856eb5 100644 --- a/src/MethodNotAllowedHandler.php +++ b/src/MethodNotAllowedHandler.php @@ -4,6 +4,7 @@ namespace Yiisoft\Router; +use InvalidArgumentException; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -17,8 +18,15 @@ class MethodNotAllowedHandler implements MethodFailureHandlerInterface public function __construct(private readonly ResponseFactoryInterface $responseFactory) { } + /** + * @throws InvalidArgumentException when methods are empty or not set + */ public function handle(ServerRequestInterface $request): ResponseInterface { + if (empty($this->methods)) { + throw new InvalidArgumentException("Allowed methods can't be empty array."); + } + return $this->responseFactory ->createResponse(Status::METHOD_NOT_ALLOWED) ->withHeader(Header::ALLOW, implode(', ', $this->methods)); diff --git a/tests/Middleware/AllowedMethodsHandlerTest.php b/tests/Middleware/AllowedMethodsHandlerTest.php new file mode 100644 index 00000000..ff084b2a --- /dev/null +++ b/tests/Middleware/AllowedMethodsHandlerTest.php @@ -0,0 +1,48 @@ +createHandler() + ->withAllowedMethods(['GET', 'HEAD']) + ->handle($this->createRequest()); + + $this->assertSame(204, $response->getStatusCode()); + $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); + } + + public function testThrownExceptionWithEmptyMethods(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Allowed methods can't be empty array."); + + $response = $this + ->createHandler() + ->withAllowedMethods([]) + ->handle($this->createRequest()); + } + + private function createHandler(): AllowedMethodsHandler + { + return new AllowedMethodsHandler(new Psr17Factory()); + } + + private function createRequest(string $uri = '/'): ServerRequestInterface + { + return new ServerRequest(Method::GET, $uri); + } +} diff --git a/tests/Middleware/MethodNotAllowedHandlerTest.php b/tests/Middleware/MethodNotAllowedHandlerTest.php new file mode 100644 index 00000000..64cfd558 --- /dev/null +++ b/tests/Middleware/MethodNotAllowedHandlerTest.php @@ -0,0 +1,48 @@ +createHandler() + ->withAllowedMethods(['GET', 'HEAD']) + ->handle($this->createRequest()); + + $this->assertSame(405, $response->getStatusCode()); + $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); + } + + public function testThrownExceptionWithEmptyMethods(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Allowed methods can't be empty array."); + + $response = $this + ->createHandler() + ->withAllowedMethods([]) + ->handle($this->createRequest()); + } + + private function createHandler(): MethodNotAllowedHandler + { + return new MethodNotAllowedHandler(new Psr17Factory()); + } + + private function createRequest(string $uri = '/'): ServerRequestInterface + { + return new ServerRequest(Method::GET, $uri); + } +} From 81fe0b1dcfe60e54e78bc407bfe97f586ebdcc5f Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Wed, 3 Sep 2025 14:58:10 +0700 Subject: [PATCH 06/17] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5583682c..2d6c2bf1 100644 --- a/README.md +++ b/README.md @@ -236,8 +236,8 @@ application middleware processes the request. ### Automatic responses `Yiisoft\Router\Middleware\Router` middleware responds automatically to: -- `OPTIONS` requests, see [OPTIONS requests](#options-requests) section below. -- Requests with methods that are not supported by the target resource, see [Method not allowed response](#method-not-allowed-response) section below. +- `OPTIONS` requests, see [OPTIONS requests](#options-requests) section. +- Requests with methods that are not supported by the target resource, see [Method not allowed response](#method-not-allowed-response) section. You can disable this behavior by calling the `Yiisoft\Router\Middleware\Router::ignoreMethodFailureHandler()` method: From 742956b4b88bdaa5106c5a474567af4ea90ce92e Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Thu, 4 Sep 2025 18:52:31 +0700 Subject: [PATCH 07/17] simplified version of mathod failure handler --- config/di.php | 5 +++- src/AllowedMethodsHandler.php | 41 --------------------------- src/MethodFailureHandler.php | 34 ++++++++++++++++++++++ src/MethodFailureHandlerInterface.php | 11 +++---- src/MethodNotAllowedHandler.php | 41 --------------------------- src/Middleware/Router.php | 28 ++---------------- 6 files changed, 47 insertions(+), 113 deletions(-) delete mode 100644 src/AllowedMethodsHandler.php create mode 100644 src/MethodFailureHandler.php delete mode 100644 src/MethodNotAllowedHandler.php diff --git a/config/di.php b/config/di.php index 8ed505f5..d1461fa8 100644 --- a/config/di.php +++ b/config/di.php @@ -2,9 +2,11 @@ declare(strict_types=1); +use Yiisoft\Router\CurrentRoute; +use Yiisoft\Router\MethodFailureHandlerInterface; +use Yiisoft\Router\MethodFailureHandler; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteCollectorInterface; -use Yiisoft\Router\CurrentRoute; return [ RouteCollectorInterface::class => RouteCollector::class, @@ -15,4 +17,5 @@ $this->arguments = []; }, ], + MethodFailureHandlerInterface::class => MethodFailureHandler::class, ]; diff --git a/src/AllowedMethodsHandler.php b/src/AllowedMethodsHandler.php deleted file mode 100644 index 1894baa4..00000000 --- a/src/AllowedMethodsHandler.php +++ /dev/null @@ -1,41 +0,0 @@ -methods)) { - throw new InvalidArgumentException("Allowed methods can't be empty array."); - } - - return $this->responseFactory - ->createResponse(Status::NO_CONTENT) - ->withHeader(Header::ALLOW, implode(', ', $this->methods)); - } - - public function withAllowedMethods(array $methods): self - { - $new = clone $this; - $new->methods = $methods; - return $new; - } -} diff --git a/src/MethodFailureHandler.php b/src/MethodFailureHandler.php new file mode 100644 index 00000000..44d3570c --- /dev/null +++ b/src/MethodFailureHandler.php @@ -0,0 +1,34 @@ +getMethod() === Method::OPTIONS + ? $this->responseFactory + ->createResponse(Status::NO_CONTENT) + ->withHeader(Header::ALLOW, implode(', ', $allowedMethods)) + : $this->responseFactory + ->createResponse(Status::METHOD_NOT_ALLOWED) + ->withHeader(Header::ALLOW, implode(', ', $allowedMethods)); + } +} diff --git a/src/MethodFailureHandlerInterface.php b/src/MethodFailureHandlerInterface.php index fb1dd03f..92793e1b 100644 --- a/src/MethodFailureHandlerInterface.php +++ b/src/MethodFailureHandlerInterface.php @@ -4,17 +4,18 @@ namespace Yiisoft\Router; -use Psr\Http\Server\RequestHandlerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; /** * `MethodFailureHandlerInterface` produces a response with a list of the target resource's supported methods. */ -interface MethodFailureHandlerInterface extends RequestHandlerInterface +interface MethodFailureHandlerInterface { /** - * Creates new instance of handler with supported methods set. + * Produces a response listing resource's allowed methods. * - * @param string[] $methods a list of the HTTP methods supported by the request's resource + * @param string[] $allowedMethods a list of the HTTP methods supported by the request's resource */ - public function withAllowedMethods(array $methods): self; + public function handle(ServerRequestInterface $request, array $allowedMethods): ResponseInterface; } diff --git a/src/MethodNotAllowedHandler.php b/src/MethodNotAllowedHandler.php deleted file mode 100644 index 94856eb5..00000000 --- a/src/MethodNotAllowedHandler.php +++ /dev/null @@ -1,41 +0,0 @@ -methods)) { - throw new InvalidArgumentException("Allowed methods can't be empty array."); - } - - return $this->responseFactory - ->createResponse(Status::METHOD_NOT_ALLOWED) - ->withHeader(Header::ALLOW, implode(', ', $this->methods)); - } - - public function withAllowedMethods(array $methods): self - { - $new = clone $this; - $new->methods = $methods; - return $new; - } -} diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index a6450fe3..7a14899f 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -10,21 +10,15 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Yiisoft\Http\Method; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\CurrentRoute; -use Yiisoft\Router\AllowedMethodsHandler; use Yiisoft\Router\MethodFailureHandlerInterface; -use Yiisoft\Router\MethodNotAllowedHandler; use Yiisoft\Router\UrlMatcherInterface; final class Router implements MiddlewareInterface { private readonly MiddlewareDispatcher $dispatcher; - private readonly ?MethodFailureHandlerInterface $allowedMethodsHandler; - private readonly ?MethodFailureHandlerInterface $methodNotAllowedHandler; - private bool $ignoreMethodFailureHandler = false; public function __construct( private readonly UrlMatcherInterface $matcher, @@ -32,12 +26,9 @@ public function __construct( MiddlewareFactory $middlewareFactory, private readonly CurrentRoute $currentRoute, ?EventDispatcherInterface $eventDispatcher = null, - ?MethodFailureHandlerInterface $allowedMethodsHandler = null, - ?MethodFailureHandlerInterface $methodNotAllowedHandler = null + private readonly ?MethodFailureHandlerInterface $methodFailureHandler = null ) { $this->dispatcher = new MiddlewareDispatcher($middlewareFactory, $eventDispatcher); - $this->allowedMethodsHandler = $allowedMethodsHandler ?? new AllowedMethodsHandler($responseFactory); - $this->methodNotAllowedHandler = $methodNotAllowedHandler ?? new MethodNotAllowedHandler($responseFactory); } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface @@ -46,14 +37,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setUri($request->getUri()); - if (!$this->ignoreMethodFailureHandler && $result->isMethodFailure()) { - return $request->getMethod() === Method::OPTIONS - ? $this->allowedMethodsHandler - ->withAllowedMethods($result->methods()) - ->handle($request) - : $this->methodNotAllowedHandler - ->withAllowedMethods($result->methods()) - ->handle($request); + if ($result->isMethodFailure() && $this->methodFailureHandler !== null) { + return $this->methodFailureHandler->handle($request, $result->methods()); } if (!$result->isSuccess()) { @@ -66,11 +51,4 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface ->withMiddlewares($result->route()->getData('enabledMiddlewares')) ->dispatch($request, $handler); } - - public function ignoreMethodFailureHandler(): self - { - $new = clone $this; - $new->ignoreMethodFailureHandler = true; - return $new; - } } From 3b0a4fdf46d170b60187663b99b7659223c0519c Mon Sep 17 00:00:00 2001 From: olegbaturin <15981018+olegbaturin@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:53:04 +0000 Subject: [PATCH 08/17] Apply Rector changes (CI) --- tests/Middleware/RouterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index abac788a..840fdd42 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -361,7 +361,7 @@ private function creatMethodFailureHandler(int $code): MethodFailureHandlerInter public function __construct(private int $code) {} - public function handle(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request, array $allowedMethods): ResponseInterface { return (new Response($this->code)) ->withHeader('Allow', implode(', ', $this->methods)) From 46f511ea3012fc2ed9474e0e5a4df8ca3968c128 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Mon, 8 Sep 2025 17:45:35 +0700 Subject: [PATCH 09/17] set default $methodFailureHandler to null --- config/di.php | 1 - src/MethodFailureHandler.php | 18 +++-- .../Middleware/AllowedMethodsHandlerTest.php | 48 ------------ .../MethodNotAllowedHandlerTest.php | 48 ------------ tests/Middleware/RouterTest.php | 75 ++++++++----------- 5 files changed, 43 insertions(+), 147 deletions(-) delete mode 100644 tests/Middleware/AllowedMethodsHandlerTest.php delete mode 100644 tests/Middleware/MethodNotAllowedHandlerTest.php diff --git a/config/di.php b/config/di.php index d1461fa8..18470bfa 100644 --- a/config/di.php +++ b/config/di.php @@ -17,5 +17,4 @@ $this->arguments = []; }, ], - MethodFailureHandlerInterface::class => MethodFailureHandler::class, ]; diff --git a/src/MethodFailureHandler.php b/src/MethodFailureHandler.php index 44d3570c..c0280f4c 100644 --- a/src/MethodFailureHandler.php +++ b/src/MethodFailureHandler.php @@ -12,9 +12,13 @@ use Yiisoft\Http\Method; use Yiisoft\Http\Status; -class MethodFailureHandler implements MethodFailureHandlerInterface +/** + * Default handler that is produces a response with a list of the target resource's supported methods. + */ +final class MethodFailureHandler implements MethodFailureHandlerInterface { - public function __construct(private readonly ResponseFactoryInterface $responseFactory) { + public function __construct(private readonly ResponseFactoryInterface $responseFactory) + { } public function handle(ServerRequestInterface $request, array $allowedMethods): ResponseInterface @@ -23,12 +27,10 @@ public function handle(ServerRequestInterface $request, array $allowedMethods): throw new InvalidArgumentException("Allowed methods can't be empty array."); } - return $request->getMethod() === Method::OPTIONS - ? $this->responseFactory - ->createResponse(Status::NO_CONTENT) - ->withHeader(Header::ALLOW, implode(', ', $allowedMethods)) - : $this->responseFactory - ->createResponse(Status::METHOD_NOT_ALLOWED) + $status = $request->getMethod() === Method::OPTIONS ? Status::NO_CONTENT : Status::METHOD_NOT_ALLOWED; + + return $this->responseFactory + ->createResponse($status) ->withHeader(Header::ALLOW, implode(', ', $allowedMethods)); } } diff --git a/tests/Middleware/AllowedMethodsHandlerTest.php b/tests/Middleware/AllowedMethodsHandlerTest.php deleted file mode 100644 index ff084b2a..00000000 --- a/tests/Middleware/AllowedMethodsHandlerTest.php +++ /dev/null @@ -1,48 +0,0 @@ -createHandler() - ->withAllowedMethods(['GET', 'HEAD']) - ->handle($this->createRequest()); - - $this->assertSame(204, $response->getStatusCode()); - $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); - } - - public function testThrownExceptionWithEmptyMethods(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Allowed methods can't be empty array."); - - $response = $this - ->createHandler() - ->withAllowedMethods([]) - ->handle($this->createRequest()); - } - - private function createHandler(): AllowedMethodsHandler - { - return new AllowedMethodsHandler(new Psr17Factory()); - } - - private function createRequest(string $uri = '/'): ServerRequestInterface - { - return new ServerRequest(Method::GET, $uri); - } -} diff --git a/tests/Middleware/MethodNotAllowedHandlerTest.php b/tests/Middleware/MethodNotAllowedHandlerTest.php deleted file mode 100644 index 64cfd558..00000000 --- a/tests/Middleware/MethodNotAllowedHandlerTest.php +++ /dev/null @@ -1,48 +0,0 @@ -createHandler() - ->withAllowedMethods(['GET', 'HEAD']) - ->handle($this->createRequest()); - - $this->assertSame(405, $response->getStatusCode()); - $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); - } - - public function testThrownExceptionWithEmptyMethods(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Allowed methods can't be empty array."); - - $response = $this - ->createHandler() - ->withAllowedMethods([]) - ->handle($this->createRequest()); - } - - private function createHandler(): MethodNotAllowedHandler - { - return new MethodNotAllowedHandler(new Psr17Factory()); - } - - private function createRequest(string $uri = '/'): ServerRequestInterface - { - return new ServerRequest(Method::GET, $uri); - } -} diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index 840fdd42..3e463c9b 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -18,6 +18,7 @@ use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Group; use Yiisoft\Router\MatchingResult; +use Yiisoft\Router\MethodFailureHandler; use Yiisoft\Router\MethodFailureHandlerInterface; use Yiisoft\Router\Middleware\Router; use Yiisoft\Router\Route; @@ -44,60 +45,63 @@ public function testMissingRouteRespondWith404(): void $this->assertSame(404, $response->getStatusCode()); } - public function testMethodMismatchRespondWith405(): void + public function testDefaultMethodFailureRespondWith404(): void { $request = new ServerRequest('POST', '/'); $response = $this->processWithRouter($request); - $this->assertSame(405, $response->getStatusCode()); - $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); + $this->assertSame(404, $response->getStatusCode()); } - public function testMethodMismatchHandlerRespondWith400(): void + public function testDefaultFailureHandlerResponseOptions(): void { - $request = new ServerRequest('POST', '/'); + $request = new ServerRequest('OPTIONS', '/'); $response = $this - ->createRouterMiddleware(methodNotAllowedHandler: $this->creatMethodFailureHandler(400)) + ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) ->process($request, $this->createRequestHandler()); - $this->assertSame(400, $response->getStatusCode()); + $this->assertSame(204, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); - $this->assertSame('JGURDA', $response->getHeaderLine('X-JGURDA')); } - public function testAutoResponseOptions(): void + public function testDefaultFailureHandlerResponseOptionsWithOrigin(): void { - $request = new ServerRequest('OPTIONS', '/'); - $response = $this->processWithRouter($request); + $request = new ServerRequest('OPTIONS', 'http://test.local/', ['Origin' => 'http://test.com']); + $response = $this + ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) + ->process($request, $this->createRequestHandler()); $this->assertSame(204, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); } - public function testAutoResponseOptionsHandler(): void + public function testCustomFailureHandlerResponseOptions(): void { $request = new ServerRequest('OPTIONS', '/'); $response = $this - ->createRouterMiddleware(allowedMethodsHandler: $this->creatMethodFailureHandler(200)) + ->createRouterMiddleware(methodFailureHandler: $this->creatMethodFailureHandler(200)) ->process($request, $this->createRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); $this->assertSame('JGURDA', $response->getHeaderLine('X-JGURDA')); } - public function testIgnoreMethodFailureHandlerRespondWith404(): void + public function testDefaultFailureHandlerRespondWith405(): void { $request = new ServerRequest('POST', '/'); $response = $this - ->createRouterMiddleware() - ->ignoreMethodFailureHandler() + ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) ->process($request, $this->createRequestHandler()); - $this->assertSame(404, $response->getStatusCode()); + $this->assertSame(405, $response->getStatusCode()); + $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); } - public function testAutoResponseOptionsWithOrigin(): void + public function testCustomFailureHandlerRespondWith400(): void { - $request = new ServerRequest('OPTIONS', 'http://test.local/', ['Origin' => 'http://test.com']); - $response = $this->processWithRouter($request); - $this->assertSame(204, $response->getStatusCode()); + $request = new ServerRequest('POST', '/'); + $response = $this + ->createRouterMiddleware(methodFailureHandler: $this->creatMethodFailureHandler(400)) + ->process($request, $this->createRequestHandler()); + $this->assertSame(400, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); + $this->assertSame('JGURDA', $response->getHeaderLine('X-JGURDA')); } public function testWithCorsHandlers(): void @@ -238,13 +242,6 @@ public function testRouteMiddleware(int $expectedCode, mixed $middleware): void $this->assertSame($expectedCode, $response->getStatusCode()); } - public function testImmutability(): void - { - $original = $this->createRouterMiddleware(); - - $this->assertNotSame($original, $original->ignoreMethodFailureHandler()); - } - private function getMatcher(?RouteCollectionInterface $routeCollection = null): UrlMatcherInterface { $middleware = $this->createRouteMiddleware(); @@ -305,8 +302,7 @@ public function createResponse(int $code = 200, string $reasonPhrase = ''): Resp private function createRouterMiddleware( ?RouteCollectionInterface $routeCollection = null, ?CurrentRoute $currentRoute = null, - ?MethodFailureHandlerInterface $allowedMethodsHandler = null, - ?MethodFailureHandlerInterface $methodNotAllowedHandler = null, + ?MethodFailureHandlerInterface $methodFailureHandler = null, array $containerDefinitions = [], ): Router { $container = new SimpleContainer( @@ -322,8 +318,7 @@ private function createRouterMiddleware( new MiddlewareFactory($container), $currentRoute ?? new CurrentRoute(), null, - $allowedMethodsHandler, - $methodNotAllowedHandler, + $methodFailureHandler ); } @@ -353,27 +348,23 @@ private function createRouteMiddleware(): callable return static fn () => new Response(201); } + private function creatDefaultMethodFailureHandler(): MethodFailureHandler + { + return new MethodFailureHandler(new Psr17Factory()); + } + private function creatMethodFailureHandler(int $code): MethodFailureHandlerInterface { return new class ($code) implements MethodFailureHandlerInterface { - private array $methods; - public function __construct(private int $code) {} public function handle(ServerRequestInterface $request, array $allowedMethods): ResponseInterface { return (new Response($this->code)) - ->withHeader('Allow', implode(', ', $this->methods)) + ->withHeader('Allow', implode(', ', $allowedMethods)) ->withHeader('X-JGURDA', 'JGURDA'); } - - public function withAllowedMethods(array $methods): self - { - $new = clone $this; - $new->methods = $methods; - return $new; - } }; } } From 042cfbac4e54f88fd83a0df28ab7f1c334104ff7 Mon Sep 17 00:00:00 2001 From: olegbaturin <15981018+olegbaturin@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:46:21 +0000 Subject: [PATCH 10/17] Apply Rector changes (CI) --- tests/Middleware/RouterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index 3e463c9b..b093dbf7 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -356,7 +356,7 @@ private function creatDefaultMethodFailureHandler(): MethodFailureHandler private function creatMethodFailureHandler(int $code): MethodFailureHandlerInterface { return new class ($code) implements MethodFailureHandlerInterface { - public function __construct(private int $code) + public function __construct(private readonly int $code) {} public function handle(ServerRequestInterface $request, array $allowedMethods): ResponseInterface From 3fdd8085adf63874e2d05a9b50e1bb4abb5ca2d7 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Mon, 8 Sep 2025 18:12:31 +0700 Subject: [PATCH 11/17] fix style --- config/di.php | 2 -- tests/Middleware/RouterTest.php | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/config/di.php b/config/di.php index 18470bfa..aefd0286 100644 --- a/config/di.php +++ b/config/di.php @@ -3,8 +3,6 @@ declare(strict_types=1); use Yiisoft\Router\CurrentRoute; -use Yiisoft\Router\MethodFailureHandlerInterface; -use Yiisoft\Router\MethodFailureHandler; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteCollectorInterface; diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index b093dbf7..01f94b8f 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -357,7 +357,8 @@ private function creatMethodFailureHandler(int $code): MethodFailureHandlerInter { return new class ($code) implements MethodFailureHandlerInterface { public function __construct(private readonly int $code) - {} + { + } public function handle(ServerRequestInterface $request, array $allowedMethods): ResponseInterface { From 55d1c018ebd3d21ce079ccf1f6bf6624e4de2ecd Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Tue, 9 Sep 2025 13:53:28 +0700 Subject: [PATCH 12/17] $methodFailureAction is a required parameter rename handler to action --- config/di.php | 8 +++ ...ureHandler.php => MethodFailureAction.php} | 2 +- ...e.php => MethodFailureActionInterface.php} | 4 +- src/Middleware/Router.php | 8 +-- tests/Middleware/MethodFailureActionTest.php | 56 +++++++++++++++++++ tests/Middleware/RouterTest.php | 30 +++++----- 6 files changed, 86 insertions(+), 22 deletions(-) rename src/{MethodFailureHandler.php => MethodFailureAction.php} (93%) rename src/{MethodFailureHandlerInterface.php => MethodFailureActionInterface.php} (74%) create mode 100644 tests/Middleware/MethodFailureActionTest.php diff --git a/config/di.php b/config/di.php index aefd0286..e28e7259 100644 --- a/config/di.php +++ b/config/di.php @@ -2,9 +2,12 @@ declare(strict_types=1); +use Yiisoft\Definitions\Reference; use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteCollectorInterface; +use Yiisoft\Router\MethodFailureAction; +use Yiisoft\Router\Middleware\Router; return [ RouteCollectorInterface::class => RouteCollector::class, @@ -15,4 +18,9 @@ $this->arguments = []; }, ], + Router::class => [ + '__construct()' => [ + 'methodFailureHandler' => Reference::to(MethodFailureAction::class), + ], + ], ]; diff --git a/src/MethodFailureHandler.php b/src/MethodFailureAction.php similarity index 93% rename from src/MethodFailureHandler.php rename to src/MethodFailureAction.php index c0280f4c..5f4f503c 100644 --- a/src/MethodFailureHandler.php +++ b/src/MethodFailureAction.php @@ -15,7 +15,7 @@ /** * Default handler that is produces a response with a list of the target resource's supported methods. */ -final class MethodFailureHandler implements MethodFailureHandlerInterface +final class MethodFailureAction implements MethodFailureActionInterface { public function __construct(private readonly ResponseFactoryInterface $responseFactory) { diff --git a/src/MethodFailureHandlerInterface.php b/src/MethodFailureActionInterface.php similarity index 74% rename from src/MethodFailureHandlerInterface.php rename to src/MethodFailureActionInterface.php index 92793e1b..667ba849 100644 --- a/src/MethodFailureHandlerInterface.php +++ b/src/MethodFailureActionInterface.php @@ -8,9 +8,9 @@ use Psr\Http\Message\ServerRequestInterface; /** - * `MethodFailureHandlerInterface` produces a response with a list of the target resource's supported methods. + * `MethodFailureActionInterface` produces a response with a list of the target resource's supported methods. */ -interface MethodFailureHandlerInterface +interface MethodFailureActionInterface { /** * Produces a response listing resource's allowed methods. diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index 7a14899f..248fd8e8 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -13,7 +13,7 @@ use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\CurrentRoute; -use Yiisoft\Router\MethodFailureHandlerInterface; +use Yiisoft\Router\MethodFailureActionInterface; use Yiisoft\Router\UrlMatcherInterface; final class Router implements MiddlewareInterface @@ -25,8 +25,8 @@ public function __construct( ResponseFactoryInterface $responseFactory, MiddlewareFactory $middlewareFactory, private readonly CurrentRoute $currentRoute, + private readonly MethodFailureActionInterface|null $methodFailureAction, ?EventDispatcherInterface $eventDispatcher = null, - private readonly ?MethodFailureHandlerInterface $methodFailureHandler = null ) { $this->dispatcher = new MiddlewareDispatcher($middlewareFactory, $eventDispatcher); } @@ -37,8 +37,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setUri($request->getUri()); - if ($result->isMethodFailure() && $this->methodFailureHandler !== null) { - return $this->methodFailureHandler->handle($request, $result->methods()); + if ($result->isMethodFailure() && $this->methodFailureAction !== null) { + return $this->methodFailureAction->handle($request, $result->methods()); } if (!$result->isSuccess()) { diff --git a/tests/Middleware/MethodFailureActionTest.php b/tests/Middleware/MethodFailureActionTest.php new file mode 100644 index 00000000..604e3834 --- /dev/null +++ b/tests/Middleware/MethodFailureActionTest.php @@ -0,0 +1,56 @@ +createHandler() + ->handle($this->createRequest(Method::OPTIONS), ['GET', 'HEAD']); + + $this->assertSame(204, $response->getStatusCode()); + $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); + } + + public function testShouldReturnCode405(): void + { + $response = $this + ->createHandler() + ->handle($this->createRequest(Method::POST), ['GET', 'HEAD']); + + $this->assertSame(405, $response->getStatusCode()); + $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); + } + + public function testThrownExceptionWithEmptyMethods(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Allowed methods can't be empty array."); + + $response = $this + ->createHandler() + ->handle($this->createRequest(), []); + } + + private function createHandler(): MethodFailureAction + { + return new MethodFailureAction(new Psr17Factory()); + } + + private function createRequest(string $method = Method::GET, string $uri = '/'): ServerRequestInterface + { + return new ServerRequest($method, $uri); + } +} diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index 01f94b8f..89e8f7ef 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -18,8 +18,8 @@ use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Group; use Yiisoft\Router\MatchingResult; -use Yiisoft\Router\MethodFailureHandler; -use Yiisoft\Router\MethodFailureHandlerInterface; +use Yiisoft\Router\MethodFailureAction; +use Yiisoft\Router\MethodFailureActionInterface; use Yiisoft\Router\Middleware\Router; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollection; @@ -45,7 +45,7 @@ public function testMissingRouteRespondWith404(): void $this->assertSame(404, $response->getStatusCode()); } - public function testDefaultMethodFailureRespondWith404(): void + public function testWithoutMethodFailureHandlerRespondWith404(): void { $request = new ServerRequest('POST', '/'); $response = $this->processWithRouter($request); @@ -56,7 +56,7 @@ public function testDefaultFailureHandlerResponseOptions(): void { $request = new ServerRequest('OPTIONS', '/'); $response = $this - ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) + ->createRouterMiddleware(methodFailureAction: $this->creatDefaultMethodFailureAction()) ->process($request, $this->createRequestHandler()); $this->assertSame(204, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -66,7 +66,7 @@ public function testDefaultFailureHandlerResponseOptionsWithOrigin(): void { $request = new ServerRequest('OPTIONS', 'http://test.local/', ['Origin' => 'http://test.com']); $response = $this - ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) + ->createRouterMiddleware(methodFailureAction: $this->creatDefaultMethodFailureAction()) ->process($request, $this->createRequestHandler()); $this->assertSame(204, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -76,7 +76,7 @@ public function testCustomFailureHandlerResponseOptions(): void { $request = new ServerRequest('OPTIONS', '/'); $response = $this - ->createRouterMiddleware(methodFailureHandler: $this->creatMethodFailureHandler(200)) + ->createRouterMiddleware(methodFailureAction: $this->creatMethodFailureAction(200)) ->process($request, $this->createRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -87,7 +87,7 @@ public function testDefaultFailureHandlerRespondWith405(): void { $request = new ServerRequest('POST', '/'); $response = $this - ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) + ->createRouterMiddleware(methodFailureAction: $this->creatDefaultMethodFailureAction()) ->process($request, $this->createRequestHandler()); $this->assertSame(405, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -97,7 +97,7 @@ public function testCustomFailureHandlerRespondWith400(): void { $request = new ServerRequest('POST', '/'); $response = $this - ->createRouterMiddleware(methodFailureHandler: $this->creatMethodFailureHandler(400)) + ->createRouterMiddleware(methodFailureAction: $this->creatMethodFailureAction(400)) ->process($request, $this->createRequestHandler()); $this->assertSame(400, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -302,7 +302,7 @@ public function createResponse(int $code = 200, string $reasonPhrase = ''): Resp private function createRouterMiddleware( ?RouteCollectionInterface $routeCollection = null, ?CurrentRoute $currentRoute = null, - ?MethodFailureHandlerInterface $methodFailureHandler = null, + ?MethodFailureActionInterface $methodFailureAction = null, array $containerDefinitions = [], ): Router { $container = new SimpleContainer( @@ -317,8 +317,8 @@ private function createRouterMiddleware( new Psr17Factory(), new MiddlewareFactory($container), $currentRoute ?? new CurrentRoute(), - null, - $methodFailureHandler + $methodFailureAction, + null ); } @@ -348,14 +348,14 @@ private function createRouteMiddleware(): callable return static fn () => new Response(201); } - private function creatDefaultMethodFailureHandler(): MethodFailureHandler + private function creatDefaultMethodFailureAction(): MethodFailureAction { - return new MethodFailureHandler(new Psr17Factory()); + return new MethodFailureAction(new Psr17Factory()); } - private function creatMethodFailureHandler(int $code): MethodFailureHandlerInterface + private function creatMethodFailureAction(int $code): MethodFailureActionInterface { - return new class ($code) implements MethodFailureHandlerInterface { + return new class ($code) implements MethodFailureActionInterface { public function __construct(private readonly int $code) { } From 1cdcffdd46187a3cde82b5ae3c33b6eb963a6a39 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Tue, 16 Sep 2025 16:15:05 +0700 Subject: [PATCH 13/17] rename MethodFailureHandler back --- composer.json | 2 +- config/di.php | 11 +++----- ...ureAction.php => MethodFailureHandler.php} | 2 +- ....php => MethodFailureHandlerInterface.php} | 4 +-- src/Middleware/Router.php | 10 +++---- tests/ConfigTest.php | 19 ++++++++++--- ...nTest.php => MethodFailureHandlerTest.php} | 8 +++--- tests/Middleware/RouterTest.php | 27 +++++++++---------- 8 files changed, 44 insertions(+), 39 deletions(-) rename src/{MethodFailureAction.php => MethodFailureHandler.php} (93%) rename src/{MethodFailureActionInterface.php => MethodFailureHandlerInterface.php} (74%) rename tests/Middleware/{MethodFailureActionTest.php => MethodFailureHandlerTest.php} (87%) diff --git a/composer.json b/composer.json index ddd318ba..2183fdf3 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", "vimeo/psalm": "^5.26.1 || ^6.8.6", - "yiisoft/di": "^1.3", + "yiisoft/di": "^1.4", "yiisoft/dummy-provider": "^1.0.1", "yiisoft/hydrator": "^1.5", "yiisoft/test-support": "^3.0.1", diff --git a/config/di.php b/config/di.php index e28e7259..7281ce08 100644 --- a/config/di.php +++ b/config/di.php @@ -2,12 +2,11 @@ declare(strict_types=1); -use Yiisoft\Definitions\Reference; use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteCollectorInterface; -use Yiisoft\Router\MethodFailureAction; -use Yiisoft\Router\Middleware\Router; +use Yiisoft\Router\MethodFailureHandler; +use Yiisoft\Router\MethodFailureHandlerInterface; return [ RouteCollectorInterface::class => RouteCollector::class, @@ -18,9 +17,5 @@ $this->arguments = []; }, ], - Router::class => [ - '__construct()' => [ - 'methodFailureHandler' => Reference::to(MethodFailureAction::class), - ], - ], + MethodFailureHandlerInterface::class => MethodFailureHandler::class, ]; diff --git a/src/MethodFailureAction.php b/src/MethodFailureHandler.php similarity index 93% rename from src/MethodFailureAction.php rename to src/MethodFailureHandler.php index 5f4f503c..c0280f4c 100644 --- a/src/MethodFailureAction.php +++ b/src/MethodFailureHandler.php @@ -15,7 +15,7 @@ /** * Default handler that is produces a response with a list of the target resource's supported methods. */ -final class MethodFailureAction implements MethodFailureActionInterface +final class MethodFailureHandler implements MethodFailureHandlerInterface { public function __construct(private readonly ResponseFactoryInterface $responseFactory) { diff --git a/src/MethodFailureActionInterface.php b/src/MethodFailureHandlerInterface.php similarity index 74% rename from src/MethodFailureActionInterface.php rename to src/MethodFailureHandlerInterface.php index 667ba849..92793e1b 100644 --- a/src/MethodFailureActionInterface.php +++ b/src/MethodFailureHandlerInterface.php @@ -8,9 +8,9 @@ use Psr\Http\Message\ServerRequestInterface; /** - * `MethodFailureActionInterface` produces a response with a list of the target resource's supported methods. + * `MethodFailureHandlerInterface` produces a response with a list of the target resource's supported methods. */ -interface MethodFailureActionInterface +interface MethodFailureHandlerInterface { /** * Produces a response listing resource's allowed methods. diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index 248fd8e8..2eaebea8 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -5,7 +5,6 @@ namespace Yiisoft\Router\Middleware; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -13,7 +12,7 @@ use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\CurrentRoute; -use Yiisoft\Router\MethodFailureActionInterface; +use Yiisoft\Router\MethodFailureHandlerInterface; use Yiisoft\Router\UrlMatcherInterface; final class Router implements MiddlewareInterface @@ -22,10 +21,9 @@ final class Router implements MiddlewareInterface public function __construct( private readonly UrlMatcherInterface $matcher, - ResponseFactoryInterface $responseFactory, MiddlewareFactory $middlewareFactory, private readonly CurrentRoute $currentRoute, - private readonly MethodFailureActionInterface|null $methodFailureAction, + private readonly MethodFailureHandlerInterface|null $MethodFailureHandler, ?EventDispatcherInterface $eventDispatcher = null, ) { $this->dispatcher = new MiddlewareDispatcher($middlewareFactory, $eventDispatcher); @@ -37,8 +35,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setUri($request->getUri()); - if ($result->isMethodFailure() && $this->methodFailureAction !== null) { - return $this->methodFailureAction->handle($request, $result->methods()); + if ($result->isMethodFailure() && $this->MethodFailureHandler !== null) { + return $this->MethodFailureHandler->handle($request, $result->methods()); } if (!$result->isSuccess()) { diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index c8f797a5..6c0597e1 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -5,11 +5,15 @@ namespace Yiisoft\Router\Tests; use Nyholm\Psr7\Uri; +use Nyholm\Psr7\Factory\Psr17Factory; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseFactoryInterface; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; use Yiisoft\Di\StateResetter; use Yiisoft\Router\CurrentRoute; +use Yiisoft\Router\MethodFailureHandler; +use Yiisoft\Router\MethodFailureHandlerInterface; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteCollectorInterface; @@ -26,6 +30,14 @@ public function testRouteCollector(): void $this->assertInstanceOf(RouteCollector::class, $routerCollector); } + public function testMethodFailureHandler(): void + { + $container = $this->createContainer(); + + $methodFailureHandler = $container->get(MethodFailureHandlerInterface::class); + $this->assertInstanceOf(MethodFailureHandler::class, $methodFailureHandler); + } + public function testCurrentRoute(): void { $container = $this->createContainer(); @@ -46,9 +58,10 @@ public function testCurrentRoute(): void private function createContainer(): Container { return new Container( - ContainerConfig::create()->withDefinitions( - $this->getContainerDefinitions() - ) + ContainerConfig::create()->withDefinitions([ + ResponseFactoryInterface::class => Psr17Factory::class, + ...$this->getContainerDefinitions(), + ]) ); } diff --git a/tests/Middleware/MethodFailureActionTest.php b/tests/Middleware/MethodFailureHandlerTest.php similarity index 87% rename from tests/Middleware/MethodFailureActionTest.php rename to tests/Middleware/MethodFailureHandlerTest.php index 604e3834..586815a3 100644 --- a/tests/Middleware/MethodFailureActionTest.php +++ b/tests/Middleware/MethodFailureHandlerTest.php @@ -10,9 +10,9 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use Yiisoft\Http\Method; -use Yiisoft\Router\MethodFailureAction; +use Yiisoft\Router\MethodFailureHandler; -final class MethodFailureActionTest extends TestCase +final class MethodFailureHandlerTest extends TestCase { public function testShouldReturnCode204(): void { @@ -44,9 +44,9 @@ public function testThrownExceptionWithEmptyMethods(): void ->handle($this->createRequest(), []); } - private function createHandler(): MethodFailureAction + private function createHandler(): MethodFailureHandler { - return new MethodFailureAction(new Psr17Factory()); + return new MethodFailureHandler(new Psr17Factory()); } private function createRequest(string $method = Method::GET, string $uri = '/'): ServerRequestInterface diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index 89e8f7ef..86f3089e 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -18,8 +18,8 @@ use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Group; use Yiisoft\Router\MatchingResult; -use Yiisoft\Router\MethodFailureAction; -use Yiisoft\Router\MethodFailureActionInterface; +use Yiisoft\Router\MethodFailureHandler; +use Yiisoft\Router\MethodFailureHandlerInterface; use Yiisoft\Router\Middleware\Router; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollection; @@ -56,7 +56,7 @@ public function testDefaultFailureHandlerResponseOptions(): void { $request = new ServerRequest('OPTIONS', '/'); $response = $this - ->createRouterMiddleware(methodFailureAction: $this->creatDefaultMethodFailureAction()) + ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) ->process($request, $this->createRequestHandler()); $this->assertSame(204, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -66,7 +66,7 @@ public function testDefaultFailureHandlerResponseOptionsWithOrigin(): void { $request = new ServerRequest('OPTIONS', 'http://test.local/', ['Origin' => 'http://test.com']); $response = $this - ->createRouterMiddleware(methodFailureAction: $this->creatDefaultMethodFailureAction()) + ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) ->process($request, $this->createRequestHandler()); $this->assertSame(204, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -76,7 +76,7 @@ public function testCustomFailureHandlerResponseOptions(): void { $request = new ServerRequest('OPTIONS', '/'); $response = $this - ->createRouterMiddleware(methodFailureAction: $this->creatMethodFailureAction(200)) + ->createRouterMiddleware(methodFailureHandler: $this->creatMethodFailureHandler(200)) ->process($request, $this->createRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -87,7 +87,7 @@ public function testDefaultFailureHandlerRespondWith405(): void { $request = new ServerRequest('POST', '/'); $response = $this - ->createRouterMiddleware(methodFailureAction: $this->creatDefaultMethodFailureAction()) + ->createRouterMiddleware(methodFailureHandler: $this->creatDefaultMethodFailureHandler()) ->process($request, $this->createRequestHandler()); $this->assertSame(405, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -97,7 +97,7 @@ public function testCustomFailureHandlerRespondWith400(): void { $request = new ServerRequest('POST', '/'); $response = $this - ->createRouterMiddleware(methodFailureAction: $this->creatMethodFailureAction(400)) + ->createRouterMiddleware(methodFailureHandler: $this->creatMethodFailureHandler(400)) ->process($request, $this->createRequestHandler()); $this->assertSame(400, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -302,7 +302,7 @@ public function createResponse(int $code = 200, string $reasonPhrase = ''): Resp private function createRouterMiddleware( ?RouteCollectionInterface $routeCollection = null, ?CurrentRoute $currentRoute = null, - ?MethodFailureActionInterface $methodFailureAction = null, + ?MethodFailureHandlerInterface $methodFailureHandler = null, array $containerDefinitions = [], ): Router { $container = new SimpleContainer( @@ -314,10 +314,9 @@ private function createRouterMiddleware( return new Router( $this->getMatcher($routeCollection), - new Psr17Factory(), new MiddlewareFactory($container), $currentRoute ?? new CurrentRoute(), - $methodFailureAction, + $methodFailureHandler, null ); } @@ -348,14 +347,14 @@ private function createRouteMiddleware(): callable return static fn () => new Response(201); } - private function creatDefaultMethodFailureAction(): MethodFailureAction + private function creatDefaultMethodFailureHandler(): MethodFailureHandler { - return new MethodFailureAction(new Psr17Factory()); + return new MethodFailureHandler(new Psr17Factory()); } - private function creatMethodFailureAction(int $code): MethodFailureActionInterface + private function creatMethodFailureHandler(int $code): MethodFailureHandlerInterface { - return new class ($code) implements MethodFailureActionInterface { + return new class ($code) implements MethodFailureHandlerInterface { public function __construct(private readonly int $code) { } From 0fd564da863be403b70b5af0c35cf0fe6602d556 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Wed, 17 Sep 2025 16:12:49 +0700 Subject: [PATCH 14/17] split di config --- composer.json | 1 + config/di-web.php | 18 ++++++++++++++++++ config/di.php | 11 ----------- tests/ConfigTest.php | 12 ++++++------ 4 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 config/di-web.php diff --git a/composer.json b/composer.json index 2183fdf3..013a6996 100644 --- a/composer.json +++ b/composer.json @@ -73,6 +73,7 @@ }, "config-plugin": { "di": "di.php", + "di-web": "di-web.php", "params": "params.php" } }, diff --git a/config/di-web.php b/config/di-web.php new file mode 100644 index 00000000..9156d48b --- /dev/null +++ b/config/di-web.php @@ -0,0 +1,18 @@ + [ + 'reset' => function () { + $this->route = null; + $this->uri = null; + $this->arguments = []; + }, + ], + MethodFailureHandlerInterface::class => MethodFailureHandler::class, +]; diff --git a/config/di.php b/config/di.php index 7281ce08..66993952 100644 --- a/config/di.php +++ b/config/di.php @@ -2,20 +2,9 @@ declare(strict_types=1); -use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteCollectorInterface; -use Yiisoft\Router\MethodFailureHandler; -use Yiisoft\Router\MethodFailureHandlerInterface; return [ RouteCollectorInterface::class => RouteCollector::class, - CurrentRoute::class => [ - 'reset' => function () { - $this->route = null; - $this->uri = null; - $this->arguments = []; - }, - ], - MethodFailureHandlerInterface::class => MethodFailureHandler::class, ]; diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 6c0597e1..4c1947bf 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -32,7 +32,7 @@ public function testRouteCollector(): void public function testMethodFailureHandler(): void { - $container = $this->createContainer(); + $container = $this->createContainer('web'); $methodFailureHandler = $container->get(MethodFailureHandlerInterface::class); $this->assertInstanceOf(MethodFailureHandler::class, $methodFailureHandler); @@ -40,7 +40,7 @@ public function testMethodFailureHandler(): void public function testCurrentRoute(): void { - $container = $this->createContainer(); + $container = $this->createContainer('web'); $currentRoute = $container->get(CurrentRoute::class); $currentRoute->setRouteWithArguments(Route::get('/main'), ['name' => 'hello']); @@ -55,19 +55,19 @@ public function testCurrentRoute(): void $this->assertSame([], $currentRoute->getArguments()); } - private function createContainer(): Container + private function createContainer(?string $postfix = null): Container { return new Container( ContainerConfig::create()->withDefinitions([ ResponseFactoryInterface::class => Psr17Factory::class, - ...$this->getContainerDefinitions(), + ...$this->getDiConfig($postfix), ]) ); } - private function getContainerDefinitions(): array + private function getDiConfig(?string $postfix = null): array { $params = require dirname(__DIR__) . '/config/params.php'; - return require dirname(__DIR__) . '/config/di.php'; + return require dirname(__DIR__) . '/config/di' . ($postfix !== null ? '-' . $postfix : '') . '.php'; } } From 85a9274d4b961a59bc21fb51ba2e5bf7b5748c23 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Fri, 26 Sep 2025 21:47:58 +0700 Subject: [PATCH 15/17] fix var names --- src/Middleware/Router.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index 2eaebea8..ebe3a3ed 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -23,7 +23,7 @@ public function __construct( private readonly UrlMatcherInterface $matcher, MiddlewareFactory $middlewareFactory, private readonly CurrentRoute $currentRoute, - private readonly MethodFailureHandlerInterface|null $MethodFailureHandler, + private readonly MethodFailureHandlerInterface|null $methodFailureHandler, ?EventDispatcherInterface $eventDispatcher = null, ) { $this->dispatcher = new MiddlewareDispatcher($middlewareFactory, $eventDispatcher); @@ -35,8 +35,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setUri($request->getUri()); - if ($result->isMethodFailure() && $this->MethodFailureHandler !== null) { - return $this->MethodFailureHandler->handle($request, $result->methods()); + if ($result->isMethodFailure() && $this->methodFailureHandler !== null) { + return $this->methodFailureHandler->handle($request, $result->methods()); } if (!$result->isSuccess()) { From 367f25029507015a06d4d749e50a9a7f9176c3c6 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Sat, 27 Sep 2025 17:06:41 +0700 Subject: [PATCH 16/17] update readme --- README.md | 154 ++++++++++---------------------------- src/Middleware/Router.php | 2 +- 2 files changed, 41 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 8abb346e..9789bc3e 100644 --- a/README.md +++ b/README.md @@ -222,10 +222,18 @@ If host is specified, all routes in the group would match only if the host match To simplify usage in PSR-middleware based application, there is a ready to use `Yiisoft\Router\Middleware\Router` middleware provided: ```php -$router = $container->get(Yiisoft\Router\UrlMatcherInterface::class); -$responseFactory = $container->get(\Psr\Http\Message\ResponseFactoryInterface::class); +use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; +use Yiisoft\Router\CurrentRoute; +use Yiisoft\Router\MethodFailureHandlerInterface; +use Yiisoft\Router\Middleware\Router; +use Yiisoft\Router\UrlMatcherInterface; -$routerMiddleware = new Yiisoft\Router\Middleware\Router($router, $responseFactory, $container); +$matcher = $container->get(UrlMatcherInterface::class); +$middlewareFactory = $container->get(MiddlewareFactory::class); +$currentRoute = $container->get(CurrentRoute::class); +$methodFailureHandler = $container->get(MethodFailureHandlerInterface::class); + +$routerMiddleware = new Router($matcher, $middlewareFactory, $currentRoute, $methodFailureHandler); // Add middleware to your middleware handler of choice. ``` @@ -233,152 +241,70 @@ $routerMiddleware = new Yiisoft\Router\Middleware\Router($router, $responseFacto When a route matches router middleware executes handler middleware attached to the route. If there is no match, next application middleware processes the request. -### Automatic responses - -`Yiisoft\Router\Middleware\Router` middleware responds automatically to: -- `OPTIONS` requests, see [OPTIONS requests](#options-requests) section. -- Requests with methods that are not supported by the target resource, see [Method not allowed response](#method-not-allowed-response) section. - -You can disable this behavior by calling the `Yiisoft\Router\Middleware\Router::ignoreMethodFailureHandler()` method: +### Handling method failure error -```php -use Yiisoft\Router\Middleware\Router; +To handle method failure error, pass an instance of `Yiisoft\Router\MethodFailureHandlerInterface` to the `Yiisoft\Router\Middleware\Router` middleware's constructor. +The [Yii Router](yiisoft/router) package provides a default method failure handler, `Yiisoft\Router\MethodFailureHandler`. -$routerMiddleware = new Router($router, $responseFactory, $middlewareFactory, $currentRoute); +`Yiisoft\Router\MethodFailureHandler` responds based on the HTTP methods supported by the request's resource: -// Returns a new instance with the turned off method failure error handler. -$routerMiddleware = $routerMiddleware->ignoreMethodFailureHandler(); +- For `OPTIONS` requests: ``` - -or define the `Yiisoft\Router\Middleware\Router` configuration in the DI container: - -`config/common/di/router.php` - -```php -use Yiisoft\Router\Middleware\Router; - -return [ - Router::class => [ - 'ignoreMethodFailureHandler()' => [], - ], -]; +HTTP/1.1 204 No Content +Allow: GET, HEAD ``` -#### OPTIONS requests - -By default, `Yiisoft\Router\Middleware\Router` middleware responds to `OPTIONS` requests based on the routes defined: - +- For requests with methods that are not supported by the target resource: ``` -HTTP/1.1 204 No Content +HTTP/1.1 405 Method Not Allowed Allow: GET, HEAD ``` -You can change this behavior by implementing your own request handler: +To use `Yiisoft\Router\MethodFailureHandler`, pass it to the `Yiisoft\Router\Middleware\Router` middleware constructor. ```php -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Yiisoft\Http\Header; -use Yiisoft\Http\Status; -use Yiisoft\Router\MethodFailureHandlerInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Yiisoft\Router\MethodFailureHandler; use Yiisoft\Router\Middleware\Router; -$allowedMethodsHandler = new class implements MethodFailureHandlerInterface { - private array $methods; - - public function handle(ServerRequestInterface $request): ResponseInterface - { - return (new Response(Status::OK)) - ->withHeader(Header::ALLOW, implode(', ', $this->methods)); - } - - public function withAllowedMethods(array $methods): self - { - $new = clone $this; - $new->methods = $methods; - return $new; - } - }; +$responseFactory = $container->get(ResponseFactoryInterface::class); +$methodFailureHandler = new MethodFailureHandler($responseFactory); -$routerMiddleware = new Router( - $router, - $responseFactory, +$middleware = new Router( + $matcher, $middlewareFactory, $currentRoute, - allowedMethodsHandler: $allowedMethodsHandler + $methodFailureHandler // pass the handler here ); ``` -or define the `Yiisoft\Router\Middleware\Router` configuration in the DI container: - -`config/common/di/router.php` +or define the `MethodFailureHandlerInterface` configuration in the [DI container](https://github.com/yiisoft/di): ```php -use Yiisoft\Definitions\Reference; -use Yiisoft\Router\Middleware\Router; -use App\AllowedMethodsHandler; +// config/web/di/router.php + +use Yiisoft\Router\MethodFailureHandler; +use Yiisoft\Router\MethodFailureHandlerInterface; return [ - Router::class => [ - '__construct()' => [ - 'allowedMethodsHandler' => Reference::to(AllowedMethodsHandler::class), - ], - ], + MethodFailureHandlerInterface::class => MethodFailureHandler::class, ]; ``` -#### `Method not allowed` response +> In case [Yii Router](yiisoft/router) package is used along with [Yii Config](https://github.com/yiisoft/config) plugin, the package is [configured](./config/di-web.php) +automatically to use `Yiisoft\Router\MethodFailureHandler`. -By default, `Yiisoft\Router\Middleware\Router` middleware responds to requests with methods that are not supported by the target resource based on the routes defined: - -``` -HTTP/1.1 405 Method Not Allowed -Allow: GET, HEAD -``` - -You can change this behavior by implementing your own request handler: +To disable method failure error handling pass `null` as the `methodFailureHandler` parameter of the `Yiisoft\Router\Middleware\Router` middleware constructor: ```php -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Yiisoft\Http\Status; -use Yiisoft\Router\MethodFailureHandlerInterface; -use Yiisoft\Router\Middleware\Router; - -$methodNotAllowedHandler = new class implements MethodFailureHandlerInterface { - public function handle(ServerRequestInterface $request): ResponseInterface - { - return (new Response(Status::BAD_REQUEST)); - } - }; - -$routerMiddleware = new Router( - $router, - $responseFactory, +$middleware = new Router( + $matcher, $middlewareFactory, $currentRoute, - methodNotAllowedHandler: $methodNotAllowedHandler + null // disables method failure handling ); ``` -or define the `Yiisoft\Router\Middleware\Router` configuration in the DI container: - -`config/common/di/router.php` - -```php -use Yiisoft\Definitions\Reference; -use Yiisoft\Router\Middleware\Router; -use App\MethodNotAllowedHandler; - -return [ - Router::class => [ - '__construct()' => [ - 'methodNotAllowedHandler' => Reference::to(MethodNotAllowedHandler::class), - ], - ], -]; -``` - ### CORS protocol If you need [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) you can add a middleware for handling it such as [tuupola/cors-middleware](https://github.com/tuupola/cors-middleware): diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index ebe3a3ed..646e945b 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -23,7 +23,7 @@ public function __construct( private readonly UrlMatcherInterface $matcher, MiddlewareFactory $middlewareFactory, private readonly CurrentRoute $currentRoute, - private readonly MethodFailureHandlerInterface|null $methodFailureHandler, + private readonly ?MethodFailureHandlerInterface $methodFailureHandler, ?EventDispatcherInterface $eventDispatcher = null, ) { $this->dispatcher = new MiddlewareDispatcher($middlewareFactory, $eventDispatcher); From 6e3f263274ca877a97bab9845b13a759ad536c4c Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Tue, 30 Sep 2025 13:41:53 +0700 Subject: [PATCH 17/17] fix typos --- src/MethodFailureHandler.php | 2 +- tests/Middleware/RouterTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MethodFailureHandler.php b/src/MethodFailureHandler.php index c0280f4c..b33423f7 100644 --- a/src/MethodFailureHandler.php +++ b/src/MethodFailureHandler.php @@ -13,7 +13,7 @@ use Yiisoft\Http\Status; /** - * Default handler that is produces a response with a list of the target resource's supported methods. + * Default handler that produces a response with a list of the target resource's supported methods. */ final class MethodFailureHandler implements MethodFailureHandlerInterface { diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index 86f3089e..21f0a691 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -76,7 +76,7 @@ public function testCustomFailureHandlerResponseOptions(): void { $request = new ServerRequest('OPTIONS', '/'); $response = $this - ->createRouterMiddleware(methodFailureHandler: $this->creatMethodFailureHandler(200)) + ->createRouterMiddleware(methodFailureHandler: $this->createMethodFailureHandler(200)) ->process($request, $this->createRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -97,7 +97,7 @@ public function testCustomFailureHandlerRespondWith400(): void { $request = new ServerRequest('POST', '/'); $response = $this - ->createRouterMiddleware(methodFailureHandler: $this->creatMethodFailureHandler(400)) + ->createRouterMiddleware(methodFailureHandler: $this->createMethodFailureHandler(400)) ->process($request, $this->createRequestHandler()); $this->assertSame(400, $response->getStatusCode()); $this->assertSame('GET, HEAD', $response->getHeaderLine('Allow')); @@ -352,7 +352,7 @@ private function creatDefaultMethodFailureHandler(): MethodFailureHandler return new MethodFailureHandler(new Psr17Factory()); } - private function creatMethodFailureHandler(int $code): MethodFailureHandlerInterface + private function createMethodFailureHandler(int $code): MethodFailureHandlerInterface { return new class ($code) implements MethodFailureHandlerInterface { public function __construct(private readonly int $code)