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

Deprecate middleware listener #51

Merged
merged 5 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,17 @@
"phpunit/phpunit": "^9.4.2"
},
"suggest": {
"http-interop/http-middleware": "^0.4.1 to be used together with laminas-stratigility",
"laminas/laminas-json": "(^2.6.1 || ^3.0) To auto-deserialize JSON body content in AbstractRestfulController extensions, when json_decode is unavailable",
"laminas/laminas-log": "^2.9.1 To provide log functionality via LogFilterManager, LogFormatterManager, and LogProcessorManager",
"laminas/laminas-mvc-console": "laminas-mvc-console provides the ability to expose laminas-mvc as a console application",
"laminas/laminas-mvc-i18n": "laminas-mvc-i18n provides integration with laminas-i18n, including a translation bridge and translatable route segments",
"laminas/laminas-mvc-middleware": "To dispatch middleware in your laminas-mvc application",
"laminas/laminas-mvc-plugin-fileprg": "To provide Post/Redirect/Get functionality around forms that container file uploads",
"laminas/laminas-mvc-plugin-flashmessenger": "To provide flash messaging capabilities between requests",
"laminas/laminas-mvc-plugin-identity": "To access the authenticated identity (per laminas-authentication) in controllers",
"laminas/laminas-mvc-plugin-prg": "To provide Post/Redirect/Get functionality within controllers",
"laminas/laminas-paginator": "^2.7 To provide pagination functionality via PaginatorPluginManager",
"laminas/laminas-psr7bridge": "(^0.2) To consume PSR-7 middleware within the MVC workflow",
"laminas/laminas-servicemanager-di": "laminas-servicemanager-di provides utilities for integrating laminas-di and laminas-servicemanager in your laminas-mvc application",
"laminas/laminas-stratigility": "(>=2.0.1 <2.2) laminas-stratigility is required to use middleware pipes in the MiddlewareListener"
"laminas/laminas-servicemanager-di": "laminas-servicemanager-di provides utilities for integrating laminas-di and laminas-servicemanager in your laminas-mvc application"
},
"autoload": {
"psr-4": {
Expand Down
159 changes: 19 additions & 140 deletions docs/book/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,152 +5,31 @@ and is now being adopted by many frameworks; Laminas itself offers a
parallel microframework targeting PSR-7 with [Mezzio](https://docs.mezzio.dev/mezzio).
What if you want to dispatch PSR-7 middleware from laminas-mvc?

laminas-mvc currently uses [laminas-http](https://github.com/laminas/laminas-http)
laminas-mvc currently uses [laminas-http](https://docs.laminas.dev/laminas-http/)
for its HTTP transport layer, and the objects it defines are not compatible with
PSR-7, meaning the basic MVC layer does not and cannot make use of PSR-7
currently.

However, starting with version 2.7.0, laminas-mvc offers
`Laminas\Mvc\MiddlewareListener`. This [dispatch](mvc-event.md#mvceventevent_dispatch-dispatch)
listener listens prior to the default `DispatchListener`, and executes if the
route matches contain a "middleware" parameter, and the service that resolves to
is callable. When those conditions are met, it uses the [PSR-7 bridge](https://github.com/laminas/laminas-psr7bridge)
to convert the laminas-http request and response objects into PSR-7 instances, and
then invokes the middleware.
Package [laminas-mvc-middleware][laminas-mvc-middleware] is a laminas-mvc
application module that enables dispatching of middleware, middleware pipes and
request handlers for the route matches that contain a `middleware` parameter.
Xerkus marked this conversation as resolved.
Show resolved Hide resolved

## Mapping routes to middleware
## Built-in Optional Support Deprecation

The first step is to map a route to PSR-7 middleware. This looks like any other
[routing](routing.md) configuration, with one small change: instead of providing
a "controller" in the routing defaults, you provide "middleware":
Starting with version 2.7.0, laminas-mvc offered now deprecated
`Laminas\Mvc\MiddlewareListener`. MiddlewareListener is always enabled but
requires optional dependencies installed to be used.
Module [laminas-mvc-middleware][laminas-mvc-middleware] transparently replaces
it with `Laminas\Mvc\Middleware\MiddlewareListener` when registered with the
laminas-mvc application.
Xerkus marked this conversation as resolved.
Show resolved Hide resolved

```php
// Via configuration:
return [
'router' =>
'routes' => [
'home' => [
'type' => 'literal',
'options' => [
'route' => '/',
'defaults' => [
'middleware' => 'Application\Middleware\IndexMiddleware',
],
],
],
],
],
];
Starting with version 3.2.0, built-in `Laminas\Mvc\MiddlewareListener` will
trigger deprecation level errors on an attempt to handle a route match with
`middleware` parameter.
Xerkus marked this conversation as resolved.
Show resolved Hide resolved

// Manually:
$route = Literal::factory([
'route' => '/',
'defaults' => [
'middleware' => 'Application\Middleware\IndexMiddleware',
],
]);
```
If your application currently depends on the built-in optional middleware
support, `laminas/laminas-mvc-middleware:~1.0.0` provides a drop-in replacement.
Note that module `Laminas\Mvc\Middleware` must be enabled in the laminas-mvc
application.
Xerkus marked this conversation as resolved.
Show resolved Hide resolved

Middleware may be provided as PHP callables, or as service names.

**As of 3.1.0** you may also specify an `array` of middleware, and middleware
may be [http-interop/http-middleware](https://github.com/http-interop/http-middleware)
compatible. Each item in the array must be a PHP callable, service name, or
http-middleware instance. These will then be piped into a
`Laminas\Stratigility\MiddlewarePipe` instance in the order in which they are
present in the array.

> ### No action required
>
> Unlike action controllers, middleware typically is single purpose, and, as
> such, does not require a default `action` parameter.

## Middleware services

In a normal laminas-mvc dispatch cycle, controllers are pulled from a dedicated
`ControllerManager`. Middleware, however, are pulled from the application
service manager.

Middleware retrieved *must* be PHP callables. The `MiddlewareListener` will
create an error response if non-callable middleware is indicated.

## Writing middleware

Prior to 3.1.0, when dispatching middleware, the `MiddlewareListener` calls it
with two arguments, the PSR-7 request and response, respectively. As such, your
middleware signature should look like the following:

```php
namespace Application\Middleware;

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

class IndexMiddleware
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
{
// do some work
}
}
```

Starting in 3.1.0, the `MiddlewareListener` always adds middleware to a
`Laminas\Stratigility\MiddlewarePipe` instance, and invokes it as
[http-interop/http-middleware](https://github.com/http-interop/http-middleware),
passing it a PSR-7 `ServerRequestInterface` and an http-interop
`DelegateInterface`.

As such, ideally your middleware should implement the `MiddlewareInterface` from
[http-interop/http-middleware](https://github.com/http-interop/http-middleware):

```php
namespace Application\Middleware;

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;

class IndexMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
// do some work
}
}
```

Alternately, you may still write `callable` middleware using the following
signature:

```php
function (ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
// do some work
}
```

In the above case, the `DelegateInterface` is decorated as a callable.

In all versions, within your middleware, you can pull information from the
composed request, and return a response.

> ### Routing parameters
>
> At the time of the 2.7.0 release, route match parameters were not yet injected
> into the PSR-7 `ServerRequest` instance, and thus not available as request
> attributes.
>
> With the 3.0 release, they are pushed into the PSR-7 `ServerRequest` as
> attributes, and may thus be fetched using
> `$request->getAttribute($attributeName)`.

## Middleware return values

Ideally, your middleware should return a PSR-7 response. When it does, it is
converted back to a laminas-http response and returned by the `MiddlewareListener`,
causing the application to short-circuit and return the response immediately.

You can, however, return arbitrary values. If you do, the result is pushed into
the `MvcEvent` as the event result, allowing later dispatch listeners to
manipulate the results.
[laminas-mvc-middleware]: https://docs.laminas.dev/laminas-mvc-middleware/
5 changes: 4 additions & 1 deletion src/Exception/InvalidMiddlewareException.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

namespace Laminas\Mvc\Exception;

final class InvalidMiddlewareException extends RuntimeException
/**
* @deprecated Since 3.2.0
*/
class InvalidMiddlewareException extends RuntimeException
{
/**
* @var string
Expand Down
3 changes: 3 additions & 0 deletions src/Exception/ReachedFinalHandlerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

namespace Laminas\Mvc\Exception;

/**
* @deprecated Since 3.2.0
*/
class ReachedFinalHandlerException extends RuntimeException
{
/**
Expand Down
17 changes: 14 additions & 3 deletions src/MiddlewareListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@
use Laminas\Mvc\Controller\MiddlewareController;
use Laminas\Mvc\Exception\InvalidMiddlewareException;
use Laminas\Psr7Bridge\Psr7Response;
use Laminas\Stratigility\Delegate\CallableDelegateDecorator;
use Laminas\Stratigility\MiddlewarePipe;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ResponseInterface as PsrResponseInterface;

use function sprintf;
use function trigger_error;

use const E_USER_DEPRECATED;

/**
* @deprecated Since 3.2.0
*/
class MiddlewareListener extends AbstractListenerAggregate
{
/**
Expand Down Expand Up @@ -52,6 +59,12 @@ public function onDispatch(MvcEvent $event)
return;
}

trigger_error(sprintf(
'Dispatching middleware with %s is deprecated since 3.2.0;'
. ' please use laminas/laminas-mvc-middleware package instead',
Xerkus marked this conversation as resolved.
Show resolved Hide resolved
self::class
), E_USER_DEPRECATED);

$request = $event->getRequest();
$application = $event->getApplication();
$response = $application->getResponse();
Expand Down Expand Up @@ -87,8 +100,6 @@ public function onDispatch(MvcEvent $event)
))->dispatch($request, $response);
} catch (\Throwable $ex) {
$caughtException = $ex;
} catch (\Exception $ex) { // @TODO clean up once PHP 7 requirement is enforced
$caughtException = $ex;
}

if ($caughtException !== null) {
Expand Down
51 changes: 51 additions & 0 deletions test/MiddlewareListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

use function error_reporting;
use function sprintf;
use function var_export;

use const E_USER_DEPRECATED;

class MiddlewareListenerTest extends TestCase
{
use ProphecyTrait;
Expand All @@ -38,6 +44,21 @@ class MiddlewareListenerTest extends TestCase
* @var \Prophecy\Prophecy\ObjectProphecy
*/
private $routeMatch;
/**
* @var int
*/
private $errorReporting;

protected function setUp(): void
{
$this->errorReporting = error_reporting();
error_reporting($this->errorReporting & ~E_USER_DEPRECATED);
}

protected function tearDown(): void
{
error_reporting($this->errorReporting);
}

/**
* Create an MvcEvent, populated with everything it needs.
Expand Down Expand Up @@ -106,6 +127,36 @@ public function testSuccessfullyDispatchesMiddleware()
$this->assertEquals('Test!', $return->getBody());
}

/**
* Stratigility v2 does not support PHP 8
* @requires PHP <8
*/
public function testDispatchingMiddlewareTriggersDeprecation(): void
{
error_reporting($this->errorReporting & E_USER_DEPRECATED);
$this->expectDeprecation();
$this->expectDeprecationMessage('use laminas/laminas-mvc-middleware');
Xerkus marked this conversation as resolved.
Show resolved Hide resolved

$this->testSuccessfullyDispatchesMiddleware();
}

/**
* @doesNotPerformAssertions
*/
public function testDeprecationIsNotTriggeredWhenMiddlewareListenerShortCircuits(): void
{
error_reporting($this->errorReporting & E_USER_DEPRECATED);

$this->routeMatch = $this->prophesize(RouteMatch::class);
$routeMatch = new RouteMatch(['middleware' => false]);

$event = new MvcEvent();
$event->setRouteMatch($routeMatch);

$listener = new MiddlewareListener();
$listener->onDispatch($event);
}

/**
* Stratigility v2 does not support PHP 8
* @requires PHP <8
Expand Down