Skip to content
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 option to ignore method failure handler to the 'Router' middleware (@olegbaturin)
- New #249 Add custom response factories for the method failure responses to the 'Router' middleware (@olegbaturin)

## 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

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;
Comment thread
olegbaturin marked this conversation as resolved.
- requests with methods that are not supported by the target resource.

You can disable this behavior by calling the `Yiisoft\Router\Middleware\Router::ignoreMethodFailureHandler()` method:

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

$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()' => [],
],
];
```

By default, router responds automatically to OPTIONS requests based on the routes defined:
#### 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 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:

```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
{
/**
* Produces a response listing resource's allowed methods.
*
* @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 $ignoreMethodFailureHandler = false;
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->ignoreMethodFailureHandler && $result->isMethodFailure()) {
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 ignoreMethodFailureHandler(): self
{
$new = clone $this;
$new->ignoreMethodFailureHandler = 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 thread
olegbaturin marked this conversation as resolved.
}

/**
* @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 thread
olegbaturin marked this conversation as resolved.
}
}
Loading