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

Provide http-interop compatibility #75

Merged

Conversation

weierophinney
Copy link
Member

This patch provides compatibility with http-interop 0.2.0, via the following features:

  • MiddlewarePipe:

    • now implements ServerMiddlewareInterface.

    • now allows piping http-interop ServerMiddlewareInterface and MiddlewareInterface instances.

    • now allows piping callable middleware with the signature:

      function ($request, DelegateInterface $delegate) : ResponseInterface
    • Now exposes a setResponsePrototype() method; when set, callable middleware implementing the legacy interface will be decorated as ServerMiddlewareInterface instances.

  • Next:

    • Now implements DelegateInterface.
  • Dispatch:

    • Now provides a process() method mimicing the DelegateInterface; this method is used by Next when dispatching ServerMiddlewareInterface instances.
    • Now identifies http-interop middleware and dispatches them properly.

Additionally, this patch provides decorator classes for decorating legacy callable middleware, callable middleware compatible with ServerMiddlewareInterface, and callable delegates. This latter feature can be used to adapt existing callable middleware classes to implement ServerMiddlewareInterface:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Middleware\DelegateInterface;
use Psr\Http\Middleware\ServerMiddlewareInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Stratigility\Delegate\CallableDelegateDecorator;

class PingMiddleware implements ServerMiddlewareInterface
{
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) {
        return $this->process($request, new CallableDelegateDecorator($next, $response));
    }

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        return new JsonResponse(['ack' => time()]);
    }
}

Finally, the patch provides migration documentation to both v1.3 and v2.0. Another patch will be submitted shortly against the dev-2.0.0 branch that incorporates the patches from this branch, with updates to provide the final 2.0.0 functionality around http-interop compatibility.

`Next` will be called via its `process()` method when invoked by
http-interop middleware; as such, it can invoke a separate method on
Dispatch.

This patch introduces `Dispatch::process()`, which then determines how
to delegate the route provided. For non-http-interop middleware, it will
raise an exception if the `Dispatch` instance does not have a response
prototype injected, or if the request is a non-server-side request. When
dispatching http-interop middleware, if an exception is caught, it
raises exceptions for the same conditions, but otherwise delegates to
the next error handler.
Extracted actual handler processing into:

- dispatchCallableMiddleware()
- dispatchInteropMiddleware()

Each now tests for the handler type received and proxies to the
appropriate method.
`Next` now implements `DelegateInterface`, to allow usage with
PSR-15/http-interop middleware.

For this to work, the following changes were made:

- `Next` now optionally allows setting a response prototype. If none is
  set when `__invoke()` is first called, the instance will memoize the
  response.
- `setResponsePrototype()` also sets the response prototype on the
  composed `Dispatch` instance.
- `process()` was added, and programmed to operate exactly as
  `__invoke()`, with the following additional behaviors/behavior
  changes:
  - it calls `Dispatch::process()` instead of `Dispatch::__invoke()`.
  - if the queue is empty:
    - if no response prototype exists, an exception is thrown
    - if the request is not a server request, an exception is thrown
    - if one exists, that value will be passed to the `$done` handler
  - if delegating to the next in the queue, it uses its own `process()`
    method, and not the `__invoke()` method.
  - if the middleware dispatched does not return a response, and no
    response prototype is present, it raises an exception.
- internal methods typehinting on `ServerRequestInterface` were updated
  to hint on `RequestInterface` instead, as they were only operating on
  methods defined in that looser interface.
`Next`:

- Allows composing a `DelegateInterface` instance as the `$done`
  property. Invokes this differently based on whether it is callable or
  a delegate, and now only raises exceptions for missing response
  prototype or invalid request type when invoking a callable.

`MiddlewarePipe`:

- Now implements http-interop `ServerMiddlewareInterface`
- Added response prototype composition; can set it, as well as test if
  one is already set.
- Updated `pipe()` to allow piping http-interop middleware.
- `__invoke()` now injects its `$response` argument as the `Next`
  response prototype.
- Added `process()`:
  - Creates a `Next` instance using the `$delegate` provided.
  - Sets the `Next` response prototype if it has one composed itself.
  - Calls `Next::process()` with the provided request.
  - Returns the result if it is a response, but otherwise the composed
    response prototype.

`Dispatch`:

- Prior to dispatching `MiddlewarePipe` middleare, it now injects a
  `MiddlewarePipe` with its own response prototype if the pipeline does
  not yet compose one.
- `$done` verbiage is retained *within* the `__invoke()` and `process()`
  methods, but the property name and `dispatchDone()` methods are
  renamed.
…when piping when possible

Backports the `CallableMiddlewareWrapper` from the dev-2.0.0 branch;
`MiddlewarePipe::pipe()` will now decorate callable middleware *if a
response prototype is present*; if not, it continues as normal.
If a Next instance is passed to CallableMiddlewareWrapper, do not close
over it, as it is already invokable with the expected arguments.
Error middleware will be removed in 2.0.0, and will be handled specially
anyways.
…ferently

`pipe()` now detects if callable middleware has two arguments, with the
second type-hinting on `DelegateInterface`; if so, it wraps it in a new
class, `CallableInteropMiddlewareWrapper`, which simply proxies to the
callable middleware.
Will not be used internally, but can be used by existing middleware in
order to adapt it for http-interop.
Added extensive section on http-interop.
Since `MiddlewarePipe` now allows piping http-interop middleware, and
version 2.0.0 will remove the `MiddlewareInterface`, these can be
written as http-interop middleware instead.
…dler

Each now implements `__invoke()`, and has it proxy to `process()` after
first wrapping the `$next` argument in a `CallableDelegateDecorator`.
Primarily, to demonstrate http-interop middleware wherever possible.
@weierophinney
Copy link
Member Author

Pinging @zendframework/community-review-team , @michaelmoussa , and @xtreamwayz for review, please.

Copy link
Contributor

@michaelmoussa michaelmoussa left a comment

Choose a reason for hiding this comment

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

Had a few comments but overall LGTM.

>
> Using the `$response` argument is unsafe when using delegation, as an inner
> layer could return an entirely different response, ignoring any changes you
> may have introduced previously. Additionally, hen manipulating the response
Copy link
Contributor

Choose a reason for hiding this comment

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

ITYM "Additionally, when" unless poultry. :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks - fixed locally, pushing shortly.

*/
private function isNotInteropMiddleware($handler, RequestInterface $request)
{
if ($handler instanceof ServerMiddlewareInterface || $handler instanceof InteropMiddlewareInterface) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Considered this for readability?

if ($this->isInteropMiddleware($handler) {
    return false;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed - nice suggestion!

);
}

printf("[%s] %s (%d)\n", get_class($throwable), $throwable->getMessage(), $throwable->getCode());
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it not be better to record this as a user warning / error (perhaps using error_log(...) rather than outputting it? Seems like an unlikely case, but in the event that it's hit, it could leak something sensitive as as plain output.

Copy link
Member Author

Choose a reason for hiding this comment

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

oof -- that was debug code. I'm a little surprised I missed it, which means that area is likely not tested. Removing now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds like an invitation to bring the 100% test coverage hammer down in this repository too, muahahahaha! :)

weierophinney added a commit to weierophinney/zend-stratigility that referenced this pull request Nov 2, 2016
Forward port fixes due to feedback from @michaelmoussa on zendframework#75

Conflicts:
	src/Dispatch.php
Copy link
Member

@geerteltink geerteltink left a comment

Choose a reason for hiding this comment

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

Just the CallableInteropMiddlewareWrapper needs a fix and probably some testing. But overall LGTM.


Starting in version 2.0.0, `MiddlewarePipe` *will no longer implement
`Zend\Stratigility\MiddlewareInterface`, and only implement the http-interop
`ServerMiddlewareInterface`*. This has several repurcussions.
Copy link
Member

Choose a reason for hiding this comment

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

repercussions ?

/**
* Proxies to the underlying callable delegate to process a request.
*
* {@inheritDocs}
Copy link
Member

Choose a reason for hiding this comment

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

Should be {@inheritDoc}.

@@ -57,37 +65,213 @@ public function __invoke(
ResponseInterface $response,
callable $next
) {
$handler = $route->handler;
if (! $this->responsePrototype) {
Copy link
Member

Choose a reason for hiding this comment

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

Missing method level docblock:

@return ResponseInterface
@throws Exception\InvalidRequestTypeException
@throws Exception\MissingResponsePrototypeException

Copy link
Member Author

Choose a reason for hiding this comment

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

In this particular case, neither of those two exceptions can occur:

  • Since were passing the $request provided to __invoke(), InvalidRequestTypeException will not be raised, as we have a server-side request.
  • Since we received a response, the response prototype will be present, so MissingResponseProtototypeException will not be thrown, either.

I will add the missing @return annotation, however!

Copy link
Member

Choose a reason for hiding this comment

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

Also, @throws only makes sense when the method itself throws that exception, not some dependency.

*/
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $middleware($request, $delegate);
Copy link
Member

Choose a reason for hiding this comment

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

return $this->middleware($request, $delegate);

Probably lacks testing as well.

use Zend\Stratigility\Exception\MissingDelegateException;
use Zend\Stratigility\Exception\MissingResponseException;
use Zend\Stratigility\MiddlewareInterface;
use Zend\Stratigility\Utils;
Copy link
Member

Choose a reason for hiding this comment

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

Unused imports

use Zend\Escaper\Escaper;
use Zend\Stratigility\Exception\MissingDelegateException;
use Zend\Stratigility\Utils;

*/
public function __construct($path, callable $handler)
public function __construct($path, $handler)
Copy link
Member

Choose a reason for hiding this comment

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

I notice an inconsistent use of @throws in dockblocks so I'm not sure if they are required or not.
e.g. in this class the constructor lacks @throws in its dockblock whereas __get() has it.

weierophinney added a commit to weierophinney/zend-stratigility that referenced this pull request Nov 3, 2016
Forward port updates from zendframework#75

Conflicts:
	src/Dispatch.php
	src/Route.php
@weierophinney
Copy link
Member Author

@xtreamwayz I've addressed all your points of feedback; thanks! Additionally, wrote tests for the CallableMiddlewareWrapper as well as the CallableInteropMiddlewareWrapper to ensure behavior is as expected.

@geerteltink
Copy link
Member

LGTM.

These needed to be passed to ReflectionMethod, with each element of the
array as discrete arguments; previously, they were passed to
ReflectionFunction, which failed as it cannot handle arbitrary
callables.
Will likely set the response prototype during instantation in
Expressive, and then use that as the default response if none is
provided to `run()`; as such, it cannot be private visibility.
weierophinney added a commit to weierophinney/zend-stratigility that referenced this pull request Nov 3, 2016
@weierophinney weierophinney merged commit 3f6df04 into zendframework:develop Nov 7, 2016
weierophinney added a commit that referenced this pull request Nov 7, 2016
weierophinney added a commit that referenced this pull request Nov 7, 2016
@weierophinney weierophinney deleted the feature/psr-15-compat branch November 7, 2016 19:32
weierophinney added a commit that referenced this pull request Nov 7, 2016
@geerteltink geerteltink mentioned this pull request Nov 7, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
v2
Complete
Development

Successfully merging this pull request may close these issues.

None yet

4 participants