Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add options to control method failure handling in the Router middleware #250

Open
wants to merge 12 commits into
base: 3.x
Choose a base branch
from
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## 3.1.1 under development

- no changes in this release.
- New #249 Add to the 'Router' middleware option to ignore method failure handle (@olegbaturin)
- New #249 Add to the 'Router' middleware custom response factories for the method failure responses (@olegbaturin)
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved

## 3.1.0 February 20, 2024

Expand Down
134 changes: 114 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -233,17 +217,127 @@ 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

In order to simplify usage in PSR-middleware based application, there is a ready to use `Yiisoft\Router\Middleware\Router` middleware provided:
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved

```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
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
application middleware processes the request.

### Automatic responses

`Yiisoft\Router\Middleware\Router` middleware responds automatically to:
- `OPTIONS` requests;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some examples or explanation are needed, since it is not clear how exactly it responds. It's documented below but I think it could be moved here somehow.

- requests with methods that are not supported by the target resource.

You can disable this behavior by calling the `Yiisoft\Router\Middleware\Router::ignoreMethodFailureHandle()` method
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved

```php
use Yiisoft\Router\Middleware\Router;

$routerMiddleware = new Router($router, $responseFactory, $middlewareFactory, $currentRoute);

// Returns a new instance with the turned off method failure error handle.
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
$routerMiddleware = $routerMiddleware->ignoreMethodFailureHandle();
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
```

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 => [
'ignoreMethodFailureHandle()' => [],
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
],
];
```

#### OPTIONS requests

By default, router responds automatically to OPTIONS requests based on the routes defined:
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 setup a custom response factory by calling the `Yiisoft\Router\Middleware\Router::withOptionsResponseFactory()` method

```php
use Yiisoft\Router\Middleware\Router;

$routerMiddleware = new Router($router, $responseFactory, $middlewareFactory, $currentRoute);
$optionsResponseFactory = new OptionsResponseFactory();

// Returns a new instance with the response factory.
$routerMiddleware = $routerMiddleware->withOptionsResponseFactory($optionsResponseFactory);
```

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 => [
'withOptionsResponseFactory()' => [Reference::to(OptionsResponseFactory::class)],
],
];
```


#### Method not allowed

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 setup a custom response factory by calling `Yiisoft\Router\Middleware\Router::withNotAllowedResponseFactory()` method
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved

```php
use Yiisoft\Router\Middleware\Router;

$routerMiddleware = new Router($router, $responseFactory, $middlewareFactory, $currentRoute);
$notAllowedResponseFactory = new NotAllowedResponseFactory();

// Returns a new instance with the response factory.
$routerMiddleware = $routerMiddleware->withNotAllowedResponseFactory($notAllowedResponseFactory);
```

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 => [
'withNotAllowedResponseFactory()' => [Reference::to(NotAllowedResponseFactory::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;
Expand Down
21 changes: 21 additions & 0 deletions src/MethodsResponseFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Router;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* `MethodsResponseFactoryInterface` produces a response with a list of the target resource's supported methods.
*/
interface MethodsResponseFactoryInterface
{
/**
* Handles allowed methods and produces a response.
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
*
* @param array $methods a list of the HTTP methods supported by the request's resource
*/
public function create(array $methods, ServerRequestInterface $request): ResponseInterface;
}
63 changes: 54 additions & 9 deletions src/Middleware/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@
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\MethodsResponseFactoryInterface;
use Yiisoft\Router\UrlMatcherInterface;

final class Router implements MiddlewareInterface
{
private MiddlewareDispatcher $dispatcher;
private bool $ignoreMethodFailureHandle = false;
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
private ?MethodsResponseFactoryInterface $optionsResponseFactory = null;
private ?MethodsResponseFactoryInterface $notAllowedResponseFactory = null;

public function __construct(
private UrlMatcherInterface $matcher,
Expand All @@ -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->ignoreMethodFailureHandle && $result->isMethodFailure()) {
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
return $request->getMethod() === Method::OPTIONS
? $this->getOptionsResponse($request, $result->methods())
: $this->getMethodNotAllowedResponse($request, $result->methods());
}

if (!$result->isSuccess()) {
Expand All @@ -58,4 +58,49 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
->withDispatcher($this->dispatcher)
->process($request, $handler);
}

public function ignoreMethodFailureHandle(): self
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
{
$new = clone $this;
$new->ignoreMethodFailureHandle = true;
return $new;
}

public function withOptionsResponseFactory(MethodsResponseFactoryInterface $optionsResponseFactory): self
{
$new = clone $this;
$new->optionsResponseFactory = $optionsResponseFactory;
return $new;
}

public function withNotAllowedResponseFactory(MethodsResponseFactoryInterface $notAllowedResponseFactory): self
{
$new = clone $this;
$new->notAllowedResponseFactory = $notAllowedResponseFactory;
return $new;
}

/**
* @param string[] $methods
*/
private function getOptionsResponse(ServerRequestInterface $request, array $methods): ResponseInterface
{
return $this->optionsResponseFactory !== null
? $this->optionsResponseFactory->create($methods, $request)
: $this->responseFactory
->createResponse(Status::NO_CONTENT)
->withHeader(Header::ALLOW, implode(', ', $methods));
Comment on lines +90 to +92
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract it to new response factory class and use it by default for optionsResponseFactory.

}

/**
* @param string[] $methods
*/
private function getMethodNotAllowedResponse(ServerRequestInterface $request, array $methods): ResponseInterface
{
return $this->notAllowedResponseFactory !== null
? $this->notAllowedResponseFactory->create($methods, $request)
: $this->responseFactory
->createResponse(Status::METHOD_NOT_ALLOWED)
->withHeader(Header::ALLOW, implode(', ', $methods));
Comment on lines +102 to +104
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract it to new response factory class and use it by default for notAllowedResponseFactory.

}
}
Loading
Loading