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

http-interop support for dev-2.0.0 #76

Merged

Conversation

weierophinney
Copy link
Member

This patch incorporates #75, providing comprehensive http-interop support for version 2.0.

In particular:

  • MiddlewarePipe:
    • no longer implements MiddlewareInterface, though it retains its __invoke() method.
    • The $out argument to __invoke() was renamed to $delegate, removes its typehint, and is no longer optional. The argument may be a callable, in which case it is assumed to have the signature function ($request, $response); it may also be an http-interop DelegateInterface implementation.
    • Requires either a response prototype or a new Zend\Stratigility\Middleware\CallableMiddlewareWrapperFactory instance to be composed in order to decorate piped callable middleware using the legacy signature; piping such middleware without one of these being present now raises an exception.
  • Next:
    • Renames the $done constructor argument to $nextDelegate, makes it optional, and now typehints against the http-interop DelegateInterface.
    • Removes the $response and $err arguments from __invoke() entirely; the method now proxies to process().
    • Removes the response prototype property; middleware is assumed to be decorated if it needs access to a response now.
  • Dispatch is removed completely.

`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.
PSR-15 implementation.

Conflicts:
	src/Dispatch.php
	src/MiddlewarePipe.php
	src/Next.php
	test/DispatchTest.php
	test/MiddlewarePipeTest.php
	test/NextTest.php
- `$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.
Handle callable http-interop middleware, and ensure all shipped
middleware implementations comply to http-interop.

Conflicts:
	doc/book/api.md
	doc/book/executing-middleware.md
	doc/book/intro.md
	doc/book/migration/to-v2.md
	src/Middleware/CallableMiddlewareWrapper.php
	src/Middleware/ErrorHandler.php
	src/Middleware/NotFoundHandler.php
	src/MiddlewarePipe.php
	src/Next.php
	test/Middleware/NotFoundHandlerTest.php
	test/MiddlewarePipeTest.php
@weierophinney
Copy link
Member Author

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

*/
private function decorateCallableMiddleware(callable $middleware)
{
$r = $this->getReflectionFunction($middleware);
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand the need for this in the #75 for BC, but for dev-2.0.0 could we not require a particular format for the callable middleware signature in order to avoid needing to use reflection here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd like to be able to accommodate existing callable middleware using the legacy interface, as there's a lot of it out there currently. Having this ability will allow us to continue interoperating with these legacy projects so that folks can migrate to http-interop.

Aside: As it stands, there's some indication that PSR-15 may in fact only cover server middleware, which means that some of the code may end up needing to change again anyways (e.g., DelegateInterface may end up type-hinting against ServerRequestInterface, and ServerMiddlewareInterface may not need the Server prefix).

Copy link
Contributor

Choose a reason for hiding this comment

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

Would we be able to accomplish that via a separate decorator (that we provide) which folks could use to wrap the legacy callable middleware before the MiddlewarePipe gets a hold of it? That way we can still support it, but we don't need to use / maintain the extra Reflection logic to figure out how to consume it (since the user would have already wrapped it with a consistent decorator).

Just a thought - I don't have any particularly strong feelings about Reflection in this case, but it jumped out at me so I figured I'd throw the idea out there.

Copy link
Member Author

Choose a reason for hiding this comment

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

So, I've been playing with this on my website codebase, to see what works and what doesn't.

Interestingly, I only need to make three changes to Zend\Expressive\Application at this point to make it work with the Stratigility v2 codebase, and everything I've already written continues to work. As in: no need to update any existing middleware at all. It "just works". I think the bigger takeaway with that, though is: should we emit a deprecation warning in v2 for these? :)

As it is, this patch (and the one in #75) already provide the decorator you mention. However, it's painful to use, as you also need to retain a ResponseInterface instance for it:

$app->pipe(new CallableMiddlewareWrapper($middleware, $response));

Because it's the second argument, if you're doing things inline, it becomes easy to forget:

$app->pipe(new CallableMiddlewareWrapper(function ($req, $res, $next) {
    /* ... */
}, $response));

Internally, this is what pipe() is doing now, based on reflection. If performance of reflection becomes an issue, developers can decorate manually.

My feeling at this point is that http-interop is likely to change in one way or another, which may necessitate changes in Stratigility as well: interfaces and signatures may change, which would require additional rewrites later. If we can allow developers to defer updating their signatures until http-interop stabilizes and later gets ratified as PSR-15, I'd like to support that.

Copy link
Member

Choose a reason for hiding this comment

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

Good to hear that with only some changes Expressive can be made compatible.

I'm wondering about the Reflection performance. Are we talking about only a few microseconds extra or a lot more?

Copy link
Member Author

Choose a reason for hiding this comment

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

Microseconds or fractions thereof. And all losses there are made up within Next, as the logic is simpler and more performant, as the patch removes the extra Dispatch layer (as all middleware is compatible with http-interop in this 2.0-facing patch).

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.

Same issue with CallableMiddlewareWrapper as in v1.3.0. Overall LGTM.

*/
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.

Missing $this:

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed; thanks!

…aled by test

Updated `process()` to assign local `$middleware` variable to
`$middleware` property value.
Ensured all behaviors work as expected.
Forward port updates from zendframework#75

Conflicts:
	src/Dispatch.php
	src/Route.php
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.
Forward port fixes for array callables as middleware.
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
Copy link
Member Author

As a point of reference, locally I updated zend-expressive as follows:

  • Updated Application:
    • constructor now sets response prototype using an empty Diactoros response
    • removes __invoke() method
    • updates run():
      • use the response prototype if no response is provided to the method; calls setResponsePrototype() with the response in either situation
      • invokes itself using the Stratigility NoopFinalHandler as the $delegate argument
  • Updated ApplicationFactory to pipe the Stratigility OriginalMessages middleware immediately after instantiation, ensuring the original* attributes are present for all requests.

With those minimal changes, I tested against my own website code. The only code I needed to change internally were several places where I was calling getOriginalRequest() to instead call getAttribute('originalRequest').

One caveat: I'd already updated my code to essentially prototype the new error handling paradigm from Stratigility 1.3/2.0. As such, the absence of a final handler did not affect my code, but it will affect other users. As such, I'd likely recommend updating to Stratigility 1.3 for the Expressive 1.1 release, while updating the Expressive skeleton to use the new error handling facilities for its next minor release. This would allow users to gradually update their code to remove reliance on the getOriginal*() methods of the Stratigility-specific PSR-7 message decorators, and then allow a later minor release to update to Stratigility 2, by which point those should be corrected already.

@geerteltink
Copy link
Member

One caveat: I'd already updated my code to essentially prototype the new error handling paradigm from Stratigility 1.3/2.0. As such, the absence of a final handler did not affect my code, but it will affect other users.

Since it affects others without making changes, wouldn't that qualify as a BC break? Maybe it's better to create a Expressive 2.0 release? I'm pretty sure some users will just run composer update and then are complaining on IRC it doesn't work anymore.

For the skeleton a major or minor release doesn't matter since, in theory, you can never cause a BC break in that package.

@weierophinney
Copy link
Member Author

Since it affects others without making changes, wouldn't that qualify as a BC break? Maybe it's better to create a Expressive 2.0 release? I'm pretty sure some users will just run composer update and then are complaining on IRC it doesn't work anymore.

I've observed the following with composer. First, let's set some conditions:

  • Project depends on foo ^1.0
  • Component foo depends on bar ^1.0`

Further, we'll assume that you've already run composer install

Now, let's say version 1.2 of foo now depends on bar 2.0, and you run composer update. What will happen here is one of two things:

  • No updates will occur.
  • Composer will attempt to update, but find that it already has bar 1.X, but that 2.X is requested to install, and raise a warning about inability to update due to constraint problems.

In the first case, you would need to run composer require foo:^1.2, which will update your project requirements, and then update (hopefully; you may run into the second case at this point).

In the second case, you can do one of:

  • composer update foo bar, which should update both. If it doesn't, your only recourse is:
  • Blow away your vendor directory, and then perform composer require foo:^1.2, which will both update the foo requirement in your project, as well as install all dependencies. Since bar is not previously installed, it now installs the 2.0 version.

I could be completely wrong about this, but IIRC, this was the precise problem we ran into that led to us creating the upgrade script in zf-apigility-admin, which removes both the composer.lock and vendor/ directory in order to allow updating to new major releases of ZF3 components.

All that said, I'm going to test this theory out before setting a concrete roadmap for updating Expressive.

@weierophinney
Copy link
Member Author

I've just done the following to test my theory about using composer update:

  • Created an Apigility project based off of version 1.2.1 of the skeleton.
  • Ran a composer update. This grabbed the latest ZF version it could possibly retrieve, which was 2.5.3; it could not grab version 3.0 due to the fact that this represented a conflict with its constraints.
  • I then ran composer require "zendframework/zendframework:^3.0". This failed, as that package requires zend-servicemanager-di 1.1.0, which conflicts with the already installed zend-servicemanager 2.7.7. In other words, an installed sub-dependency was causing a constraint conflict when attempting to upgrade.
  • Running rm -Rf composer.lock vendor/ && composer require "zendframework/zendframework:^3.0" then succeeded (with the caveat that I had to remove zftool as a dev requirement, as that library is not compatible with 3.0 releases yet).

This seemed to verify my assertion. However, it is not exactly the scenario we'd have with Expressive and Stratigility, so I decided to try something else.

I created two packages:

  • "test/bar", a standalone package with no dependencies.
  • "test/foo", a package depending on "test/bar".

I then created a project that depends on "test/foo". To begin, I tagged both packages as 1.0.0, and had "test/foo" depend on "test/bar" ^1.0. A composer install gave me my initial dependencies.

Next, I tagged "test/bar" as 2.0.0. Running composer update in the project did nothing; there was nothing new it could install per the existing constraints.

I then updated "test/foo" to depend on "test/bar" at ^2.0, and tagged a 1.1 version of "test/foo".

When I returned to the project and ran composer update, it updated both dependencies, which is what @xtreamwayz was asserting would happen.

As such, I need to reevaluate how we will start folding these features into Expressive. I have a few ideas, but need to prototype them. I'll outline these below to give some ideas, but they should not affect how and when this particular patch for Stratigility is evaluated, merged, and eventually tagged. (I'll copy this information to a gist or a project card soon as well.)

Register error middleware by default

The first option is to simply register the various error middleware by default.

We could override the Application constructor to register both the OriginalMessages and ErrorHandler middleware by default, exposing the ErrorHandler via a getter, and thus allowing attachment of listeners. We could also override its __invoke() method to pipe the NotFoundHandler into itself if not already piped.

The main problem with the above is that doing so makes this less configurable; we cannot have an alternate response generator if we hard-code creation of one into the Application constructor.

To solve that problem, we can handle the error handler aspect in the ApplicationFactory instead. Essentially, after instantiation of the Application instance, we'd do the following:

$application->pipe(OriginalMessages::class);
$application->pipe($container->get(ErrorHandler::class);

Notice the second pipe() statement uses a service instead. Expressive would then provide a default factory for that service, and developers could either add delegators to that, or override the service entirely to provide their own.

The problem with this approach is ensuring that the ErrorHandler class is configured properly in the application configuration. This could potentially be resolved by doing something like this, however:

$errorHandler = $container->has(ErrorHandler::class)
    ? $container->get(ErrorHandler::class)
    : (new ErrorHandlerFactory())($container, ErrorHandler::class);

(Will need to check if the above is valid in PHP 5.6.)

My take is that this approach is likely the most backwards compatible.

Delegator factories

On the subject of delegator factories, we could also implement the new functionality completely via delegator factories, instead of updating the ApplicationFactory, but only after some refactoring of that factory first.

Essentially, the ApplicationFactory would only be responsible for creating the Application instance with its router, container, final handler, and emitter; it would not setup configuration-based routing. (Note: the final handler would need to be swapped for the NoopFinalHandler, instead of the legacy one.) We would then add delegator factories for:

  • Injecting the OriginalMessages middleware.
  • Injecting the ErrorHandler middleware.
  • Injecting the configuration-driven pipline and routing middleware

Application's process() method would need to check if the last item in the queue (not sure if this is top() or bottom(), but one of those allows "peeking" at the item at one or the other end of the queue, without dequeueing), and, if that item is not the NotFoundHandler, pipe that middleware to the application. (If the user has piped something else, piping the NotFoundHandler should not affect execution anyways!)

The problem with this approach is that users then need to register those delegator factories within their existing applications in order to ensure the functionality is properly registered. This essentially makes the approach, while technically correct, a no-go for existing users.

@weierophinney weierophinney merged commit cddbdf5 into zendframework:dev-2.0.0 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/http-interop-v2 branch November 7, 2016 19:49
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

3 participants