Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Refactor pipe #134

Merged
merged 13 commits into from
Jan 15, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,67 @@ Versions prior to 1.0 were originally released as `phly/conduit`; please visit
its [CHANGELOG](https://github.com/phly/conduit/blob/master/CHANGELOG.md) for
details.

## 3.0.0alpha2 - TBD

### Added

- [#134](https://github.com/zendframework/zend-stratigility/pull/134) adds a new
class, `Zend\Stratigility\Middleware\PathMiddlewareDecorator`, which provides
path segregation functionality for middleware, replacing the functionality
that was previously implemented in `MiddlewarePipe` and `Next`. Middleware
decorated in a `PathMiddlewareDecorator` will only be processed if the current
request URI path matches the path prefix provided to the decorator; if it does
match, the request passed to it will strip the path prefix from the URI.

```php
// Only process $middleware if the request path matches '/foo':
$pipeline->pipe(new PathMiddlewareDecorator('/foo', $middleware));
```

Additionally, the patch provides a utility function,
`Zend\Stratigility\path()`, to simplify the above declaration:

```php
$pipeline->pipe(path('/foo', $middleware));
```

### Changed

- [#134](https://github.com/zendframework/zend-stratigility/pull/134) marks the
`MiddlewarePipe` class as `final`, disallowing direct extension. Either
compose an instance, or create a custom PSR-15 `MiddlewareInterface`
implementation.

- [#134](https://github.com/zendframework/zend-stratigility/pull/134) updates
`MiddlewarePipe` to implement `Interop\Http\Server\RequestHandlerInterface`.
Calling it will cause it to pull the first middleware off the queue and create
a `Next` implementation that uses the remaining queue as the request handler;
it then processes the middleware.

- [#134](https://github.com/zendframework/zend-stratigility/pull/134) removes
the ability to specify a path when calling `pipe()`; use the new
`PathMiddlewareDecorator` or `path()` utility function to pipe middleware with
path segregation.

### Deprecated

- Nothing.

### Removed

- [#134](https://github.com/zendframework/zend-stratigility/pull/134) removes
the class `Zend\Stratigility\Route`. This was an internal message passed
between a `MiddlewarePipe` and `Next` instance, and its removal should not
affect end users.

- [#134](https://github.com/zendframework/zend-stratigility/pull/134) removes
`Zend\Stratigility\Exception\InvalidMiddlewareException`, as the exception is
no longer raised by `MiddlewarePipe`.

### Fixed

- Nothing.

## 3.0.0alpha1 - 2018-01-10

### Added
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"psr/http-message-implementation": "Please install a psr/http-message-implementation to consume Stratigility; e.g., zendframework/zend-diactoros"
},
"autoload": {
"files": [
"src/functions/path.php"
],
"psr-4": {
"Zend\\Stratigility\\": "src/"
}
Expand Down
116 changes: 71 additions & 45 deletions docs/book/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,16 @@ has been discussed previously. Its API is:
```php
namespace Zend\Stratigility;

use Interop\Http\Server\MiddlewareInterface as ServerMiddlewareInterface;
use Interop\Http\Server\RequestHandlerInterface as DelegateInterface;
use Interop\Http\Server\MiddlewareInterface;
use Interop\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class MiddlewarePipe implements ServerMiddlewareInterface
class MiddlewarePipe implements MiddlewareInterface, RequestHandlerInterface
{
public function pipe(
string|ServerMiddlewareInterface $path,
ServerMiddlewareInterface $middleware = null
);
public function pipe(MiddlewareInterface $middleware);

public function handle(ServerRequestInterface $request) : ResponseInterface;

public function process(
ServerRequestInterface $request,
Expand All @@ -36,52 +35,35 @@ class MiddlewarePipe implements ServerMiddlewareInterface
}
```

`pipe()` takes up to two arguments. If only one argument is provided,
`$middleware` will be assigned that value, and `$path` will be re-assigned to
the value `/`; this is an indication that the `$middleware` should be invoked
for any path. If `$path` is provided, the `$middleware` will only be executed
for that path and any subpaths.

> ### Request path changes when path matched
>
> When you pipe middleware using a path (other than '' or '/'), the middleware
> is dispatched with a request that strips the matched segment(s) from the start
> of the path.
>
> If, for example, you executed `$pipeline->pipe('/api', $api)`, and this was
> matched via a URI with the path `/api/users/foo`, the `$api` middleware will
> receive a request with the path `/users/foo`. This allows middleware
> segregated by path to be re-used without changes to its own internal routing.

Middleware is executed in the order in which it is piped to the
`MiddlewarePipe` instance.

The `MiddlewarePipe` is itself middleware, and can be executed in stacks that
expect http-interop middleware signatures.
expect http-interop middleware signatures. It is also a request handler,
allowing you to use it in paradigms where a request handler is required; when
executed in this way, it will process itself in order to generate a response.

Middleware should either return a response, or the result of
`RequestHandlerInterface::handle()` (which should eventually evaluate to a
response instance).

Within Stratigility, `Zend\Stratigility\Next` provides an implementation
of `RequestHandlerInterface`.

Internally, during execution of the `process()` method, `MiddlewarePipe` creates
an instance of `Zend\Stratigility\Next` (feeding it its queue), executes it, and
returns its response.
Internally, `MiddlewarePipe` creates an instance of `Zend\Stratigility\Next` to
use as a `RequestHandlerInterface` implementation to pass to each middleware;
`Next` receives the queue of middleware from the `MiddlewarePipe` instance and
processes each one, calling them with the current request and itself, advancing
its internal pointer until all middleware are executed, or a response is
returned.

## Next

`Zend\Stratigility\Next` is primarily an implementation detail of middleware,
and exists to allow delegating to middleware registered later in the stack. It
is implemented as an http-interop/http-middleware `RequestHandlerInterface`.
`Zend\Stratigility\Next` is primarily an implementation detail, and exists to
allow delegating to middleware aggregated in the `MiddlewarePipe`. It is
implemented as an http-interop/http-middleware `RequestHandlerInterface`.

Since your middleware needs to return a response, it must:

- Compose a response prototype in the middleware to use to build a response, or a
canned response to return, OR
- Create and return a concrete response type, OR
- Operate on a response returned by invoking the delegate.
Since your middleware needs to return a response, the instance receives the
`$handler` argument passed to `MiddlewarePipe::process()` as a fallback request
handler; if the last middleware in the queue calls on its handler, `Next` will
execute the fallback request handler to generate a response to return.

### Providing an altered request:

Expand All @@ -104,10 +86,13 @@ function ($request, RequestHandlerInterface $handler) use ($bodyParser)
{
$bodyParams = $bodyParser($request);

// Provide a new request instance to the delegate:
return $handler->handle(
// Provide a new request instance to the handler:
$response = return $handler->handle(
$request->withBodyParams($bodyParams)
);

// Return a response with an additional header:
return $response->withHeader('X-Completed', 'true');
}
```

Expand Down Expand Up @@ -137,14 +122,14 @@ function ($request, RequestHandlerInterface $handler) use ($prototype)

If your middleware is not capable of returning a response, or a particular path
in the middleware cannot return a response, return the result of executing the
delegate.
handler.

```php
return $handler->handle($request);
```

**Middleware should always return a response, and, if it cannot, return the
result of delegation.**
result of delegating to the request handler.**

### Raising an error condition

Expand Down Expand Up @@ -173,6 +158,27 @@ your users.

Stratigility provides several concrete middleware implementations.

### PathMiddlewareDecorator

If you wish to segregate middleware by path prefix and/or conditionally
execute middleware based on a path prefix, decorate your middleware using
`Zend\Stratigility\Middleware\PathMiddlewareDecorator`.

Middleware decorated by `PathMiddlewareDecorator` will only execute if the
request URI matches the path prefix provided during instantiation.

```php
// Only process $middleware if the URI path prefix matches '/foo':
$pipeline->pipe(new PathMiddlewareDecorator('/foo', $middleware));
```

When the path prefix matches, the `PathMiddlewareDecorator` will strip the path
prefix from the request passed to the decorated middleware. For example, if you
executed `$pipeline->pipe('/api', $api)`, and this was matched via a URI with
the path `/api/users/foo`, the `$api` middleware will receive a request with the
path `/users/foo`. This allows middleware segregated by path to be re-used
without changes to its own internal routing.

### CallableMiddlewareDecorator

`Zend\Stratigility\Middleware\CallableMiddlewareDecorator` provides the ability
Expand All @@ -183,7 +189,7 @@ creation when creating your pipeline:
```php
$pipeline->pipe(new CallableMiddlewareDecorator(function ($req, $handler) {
// do some work
$response = $next($req, $handler);
$response = $handler->handle($req);
// do some work
return $response;
});
Expand Down Expand Up @@ -233,3 +239,23 @@ These two middleware allow you to provide handle PHP errors and exceptions, and
This callable middleware can be used as the outermost layer of middleware in
order to set the original request and URI instances as request attributes for
inner layers.

## Utility Functions

Stratigility provides the following utility functions.

### path

````
function Zend\Stratigility\path(
string $pathPrefix,
Interop\Http\Server\MiddlewareInterface $middleware
) : Zend\Stratigility\Middleware\PathMiddlewareDecorator
```

`path()` provides a convenient way to perform path segregation when piping your
middleware.

```php
$pipeline->pipe(path('/foo', $middleware));
```
100 changes: 99 additions & 1 deletion docs/book/creating-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,102 @@ class MyMiddleware implements MiddlewareInterface
}
```

> TODO: Should be possible to use also callbacks via wrappers?
## Anonymous middleware

For one-off middleware, particularly when debugging, you can use an anonymous
class to implement `MiddleareInterface`:

```php
$pipeline->pipe(new class implements MiddlewareInterface {
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$response = $handler->handle($request);
return $response->withHeader('X-Clacks-Overhead', 'GNU Terry Pratchett');
}
});
```

## Callable middleware

Sometimes it's easier to eschew the `MiddlewareInterface`, particularly when
creating a one-off middleware for debugging purposes. In those cases, you can
create a PHP callable that follows the same signature of
`MiddlewareInterface::process()`, and wrap it in a
`Zend\Stratigility\Middleware\CallableMiddlewareDecorator` instance:

```php
$pipeline->pipe(new CallableMiddlewareDecorator(function ($req, $handler) {
// do some work
$response = $handler->($req);
// do some work
return $response;
});
```

The typehints for the arguments are optional, but such callable middleware will
receive `ServerRequestInterface` and `RequestHandlerInterface` instances,
in that order.

## Double-Pass middleware

Prior to PSR-15, many PSR-7 frameworks and projects adopted a "double-pass"
middleware definition:

```php
function (
ServerRequestInterface $request,
ResponseInterface $response,
callable $next
) : ResponseInterface
```

where `$next` had the signature:

```php
function (
ServerRequestInterface $request,
ResponseInterface $response
) : ResponseInterface
```

The latter is the origin of the term "double-pass", as the implementation passes
not a single argument, but two. (The `$response` argument was often used as a
response prototype for middleware that needed to return a response.)

`Zend\Stratigility\Middleware\DoublePassMiddlewareDecorator` allows decorating
such middleware within a PSR-15 `MiddlewareInterface` implementation, allowing
it to be used in your Stratigility application.

When using `DoublePassMiddlewareDecorator`, internally it will decorate the
`$handler` instance as a callable.

To use the decorator, pass it the double-pass middleware to decorate via the
constructor:

```php
$pipeline->pipe(new DoublePassMiddlewareDecorator($middleware));
```

If you are not using zend-diactoros for your PSR-7 implementation, the decorator
also accepts a second argument, a PSR-7 `ResponseInterface` prototype instance
to pass to the double-pass middleware:

```php
$pipeline->pipe(new DoublePassMiddlewareDecorator(
$middleware,
$responsePrototype
));
```

> ### Beware of operating on the response
>
> In many cases, poorly written double-pass middleware will manipulate the
> response provided to them and pass the manipulated version to `$next`.
>
> This is problematic if you mix standard PSR-15 and double-pass middleware, as
> the response instance is dropped when `$next` is called, as the decorator we
> provide will ignore the argument.
>
> If you notice such issues appearing, please report them to the project
> providing the double-pass middleware, and ask them to only operate on the
> returned response.
Loading