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

[PSR-15] Provide middleware dispatch examples #996

Merged

Conversation

weierophinney
Copy link
Contributor

@weierophinney weierophinney commented Jan 4, 2018

Per discussions on #987, on the mailing list, and in the PSR-15 working group, this patch builds on #987 to add a new section 6.3 to the meta document, detailing "Example Interface Interactions".

The section covers the following:

  • A queue-based request handler that aggregates and dispatches middleware.
  • A decorator-based request handler for building a layered dispatch mechanism.
  • Two examples of re-usable middleware to demonstrate de-coupling from the application and the request handler.

This patch incorporates feedback made on #987, but not incorporated there.

@weierophinney
Copy link
Contributor Author

Ping @Crell — I think the new section answers your questions, but would like for you to review and confirm.

Also pinging @shadowhand — do the changes look okay for you? Also, I rebased off of master, which picked up changes merged since you opened #987.


- Middleware does not need to know anything about any other middleware or how it
is composed in the application.
- The `QueueRequestHandler` is agnostic of the PSR-7 implementation in use
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing period on this line.

This system has essentially two request handlers, one that will produce a
response if no middleware generates one, and one for dispatching the middleware
layers. (In this example, the `RoutingMiddleware` will likely execute composed
handlers on a successful route match; see more on that below.)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really see 2 handlers here... I see one handler ($app) that will end up invoked 2-3 times. Am I missing something or is this worded awkwardly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, you're correct. The original example I created composed another request handler instead of a request prototype, and delegated to that handler if the last middleware in the queue calls on the handler instead of returning a response. I'm going to refactor the example to to do that, as it also then answers the "request prototype" question you have elsewhere.

}
}

$innerHandler = new class ($responsePrototype) implements RequestHandlerInterface {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: $responsePrototype isn't defined yet. Is this supposed to be a new Response())->withStatus(500)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could be, or a 404 status. I'll update to make this more clear.

Note the use of a "response prototype" in the above example; this approach
allows the middleware to be agnostic of which PSR-7 implementation is in use,
and yet still return a response in cases where it determines it should not
delegate to the handler.Similarly, it is not concerned with how the request
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing space on this line.

these to work in either situation, we need to write them such that they interact
appropriately.

Generally speaking, implementors of middleware should follow these guidelines:
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like these guidelines should be moved to the spec proper, in some form, as they directly inform how middlewares will interoperate. Eg, when to call the next handler cannot be captured in an interface but is still relevant, and if done "wrong" will lead to a non-interoperable implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@shadowhand What do you think about moving these into the spec proper? If we do, where should they live?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should but putting code samples in the spec itself. Having the guidelines at the top of the spec would be good though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I was referring to the guidelines, (probably written a bit differently). I agree the code samples should remain in the metadoc.

Copy link
Member

Choose a reason for hiding this comment

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

I really think the spec should not dictate how middlewares should behave (or other implementation details), e.g. when to generate a response or call the delegate.

Maybe that's not what you meant, but the guidelines below are too precise IMO.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with @mnapoli: I don't think the spec document should contain these guidelines:
they are very specific and detailed, and they don't actually describe the middleware themselves, they describe how to compose and dispatch them, which is a non-goal of the spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mnapoli and @stefanotorresi — thanks; I think that's great feedback, and I tend to agree. The guidelines presented are implementation details and good practices, but do not need to be followed in order to comply with the specification. I'll leave them where they are.

Copy link
Member

Choose a reason for hiding this comment

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

Could you make that part clear in the text? Because it reads like a specification (with the "should" keyword).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure what other word to user here, TBH, and the fact that it's not ALL CAPS means it does not have the same semantic weight per RFC 2119. Maybe the following?

Implementors of middleware striving for maximum interoperability may want to consider the following guidelines:

Would that work for you, @mnapoli ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated!


return $corsResult->isCorsScoped()
? $corsResult->prepareResponse($this->responsePrototype)
: $response;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's cool to see someone besides me that uses/likes that particular formatting idiom. 😄

@@ -275,6 +276,10 @@ and a request handler and must return a response. The middleware may:
- Create and return a response without passing the request to the request handler,
thereby handling the request itself.

It is expected that a middleware dispatching system will use the request handler
to delegate response creation to the next available middleware. The last middleware
would then act as a domain gateway to execute application code.
Copy link
Contributor

@Crell Crell Jan 5, 2018

Choose a reason for hiding this comment

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

This paragraph... doesn't quite make sense. I can't wrap my head around what it's saying. Let me try to rephrase and tell me if I'm in the right ballpark:

When delegating from one middleware to the next in a sequence, a dispatching system should use an intermediary request handler as a way to link two middlewares together. The final or innermost middleware acts as a gateway to application code that will, in most cases, be responsible for response generation.

Is that accurate? (Feel free to steal liberally if so.) Should that be in the main spec in some form?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's correct, except that the "application code that will, in most cases, be responsible for response generation" will likely either be the middleware itself, or the a request handler. I'll see if I can come up with a re-phrase that makes sense.

@Crell
Copy link
Contributor

Crell commented Jan 5, 2018

This is a huge improvement, but still some bits aren't as direct as they could be. See commentary above.

*
* An HTTP middleware component participates in processing an HTTP message;
* for example, by acting on the request, generating the response, or forwarding the
* request to a subsequent middleware and possibly acting on its response.
Copy link
Member

Choose a reason for hiding this comment

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

👍


The two interfaces, `RequestHandlerInterface` and `MiddlewareInterface`, were
designed to work in conjunction with one another. While a
`RequestHandlerInterface` can feasibly be used in isolation, it gains more power
Copy link
Contributor

Choose a reason for hiding this comment

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

This sentence isn't necessary, imo. RequestHandler is perfectly viable without middleware, not the other way around.

a new response by manipulating the one returned (e.g., `return
$response->withHeader('X-Foo-Bar', 'baz')`).

The `CorsMiddleware` is one that will exercise all three of these guidelines: if
Copy link
Contributor

Choose a reason for hiding this comment

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

I am mildly uncomfortable using a highly specific example like this. This assumes that I know what CORS is and what the failure conditions mean. If we're going to use a specific example perhaps something like routing would be more generally understood?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll see if I can come up with another example. This was one I liked because it hits all the various ways middleware might produce a response:

  • on its own, due to a failed pre-condition
  • by acting as a pass-through to the request handler
  • by modifying the response returned by the request handler

Let me see if I can come up with an example, and I'll ping you if/when I do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Came up with an example using authorization instead of CORS; please review and let me know what you think.

handler is implemented; it merely uses it to produce a response when
pre-conditions have been met.

The `RoutingMiddleware` implementation described below follows a similar
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes this is better, but I think the process should be:

  • If routing matches, continue processing.
  • Else return a HTTP Not Found response.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's actually exactly how we do it in Expressive. 👍

The only issue I see with that is that it complicates the example: I then need to add dispatch middleware and a response prototype or factory. I felt the "map route to handler" approach was sufficiently simple and didn't require extra explanation.

* An HTTP middleware component participates in processing an HTTP message,
* either by acting on the request, generating the response, or forwarding the
* An HTTP middleware component participates in processing an HTTP message;
* for example, by acting on the request, generating the response, or forwarding the
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the change of phrasing? "Either" seems like the appropriate wording.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was based on feedback from @mnapoli, IIRC.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems fine. If we want to get super pedantic then "either" is technically only correct when there's 2 options, and in this case there are 3. "For example" seems OK to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or, technically, with the semicolon we could omit that word entirely and it would still be a grammatically valid list of the available options.

`s/either by/for example, by/`, with some additional punctuation.
Added a section to demonstrate both two potential middleware dispatch
systems (and thus how middleware and handlers interact), as well as
examples of re-usable middleware.
Instead of a "response prototype", we instead compose a fallback handler
that is invoked with the request if the last middleware in the
application delegates to the handler.
Per @Crell, the "decorated request handler" example included an
anonymous class that composed a `$responsePrototype`... which was not
declared elsewhere in the example. This adds a line to declare it, along
with a description of different forms it might take.
Removes two sentences that are unnecessary, and potentially
contradictory to the specification.
As @shadowhand points out, CORS is a domain many specification readers
may not be familiar with. As such, refactored it to be an authorization
middleware, with the following behavior:

- If no authorization is required, it is a pass-through proxy to the
  handler.
- If authorization is required, and the request is invalid, it returns
  an "unauthorized" response.
- Otherwise, it passes the request to the handler, and signs the
  response returned.

This approach hits the same points I was trying to make with the CORS
example, but with a domain more people will be familiar with.
Updated per verbiage from @Crell, with additional clarifications.
system should use an intermediary request handler as a way to link middleware
together. The final or innermost middleware will act as a gateway to application
code and generate a response from its results; alternately, the middleware may
delegate this responsibility to a _request handler_.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Crell This is the updated text, using your suggestion as a starting point.

Copy link
Contributor

Choose a reason for hiding this comment

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

Much clearer! Why is the last "request handler" set off, though? Perhaps "to a dedicated request handler"? (Nitpicking here.)

A statement of this sort seems like it belongs in the spec. It's not a "why and background", it's a "no, really, this is how you're supposed to use it". That seems like it should be in the spec proper.

the request to the handler, and sign the response returned based on the request.

```php
class AuthorizationMiddleware implements MiddlewareInterface
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@shadowhand Let me know if this altered example (replacing CorsMiddleware with AuthorizationMiddleware) makes more sense. I definitely want to demonstrate multiple middleware, and particularly different approaches middleware can take both to be re-usable as well as to change the workflow/paths of an application.


public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
if (! $authorizationMap->requestRequiresAuthorization($request)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How about doesRequireAuthorization instead of requestRequiresAuthorization?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe needsAuthorization to keep it short? (for easiness of readability)

$this->responsePrototype = $responsePrototype;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

ResponseInterface $responsePrototype
) {
$this->authorizationMap = $authorizationMap;
$this->responsePrototype = $responsePrototype;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure why we need a response prototype here. I think it would make more sense of the AuthorizationMap was responsible for generating/manipulating the response. That would simplify the example and keep the focus on the middleware.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed; making that change now.

these to work in either situation, we need to write them such that they interact
appropriately.

Generally speaking, implementors of middleware should follow these guidelines:
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should but putting code samples in the spec itself. Having the guidelines at the top of the spec would be good though.

public function __construct(
AuthorizationMap $authorizationMap,
ResponseInterface $responsePrototype
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

There's only 2 parameters. No need to go with the harder-to-read multi-line approach. Icky.

Copy link
Contributor

Choose a reason for hiding this comment

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

When using objects as type, I prefer the multi-line approach. Easier to read and less churn on diffs when parameters change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was going to disagree, but when I re-formatted, I realized the line is still shorter than the process() method declaration line. About to commit that now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Plus, with the change to remove the response prototype, it's now only one argument anyways...

Removes emphasis from "request handler", based on feedback.
Additionally, adds the word "dedicated", also per feedback.
… spec

Per @Crell, this patch moves the paragraph describing the interactions
between middleware and request handlers into the spec, under section
1.2.
Mainly, `): ReturnType` vs `) : ReturnType`.
- `requiresRequestAuthorization()` renamed to `needsAuthorization()`.
- Removes response prototype from AuthorizationMiddleware, as it's no longer used.
- Reformats AuthorizationMiddleware constructor declaration from multi-line to single line; shorter now, due to only one required argument.
@weierophinney
Copy link
Contributor Author

Requested changes have been made (mostly):

  • I did not move the "guidelines" bullet points to the spec; as several folks noted, coding to those guidelines will definitely mean you're coding to the spec, but you do not need to follow them to implement the spec.
  • I removed the emphasis around "request handler", and added the word "dedicated" before them, per @Crell.
  • I moved that particular paragraph to section 1.2 of the spec, per @Crell.
  • I updated the preamble to MiddlewareInterface to incorporate the various bits of feedback.
  • I updated code samples to follow PSR-12.
  • I made changes to the AuthorizationMiddleware example per feedback received.

@Crell, @shadowhand, @mnapoli and @stefanotorresi — can you give one more review so we can finalize these? I think we can then prepare for a CC vote.

Copy link
Contributor

@shadowhand shadowhand left a comment

Choose a reason for hiding this comment

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

My concerns are addressed.

system should use an intermediary request handler as a way to link middleware
together. The final or innermost middleware will act as a gateway to application
code and generate a response from its results; alternately, the middleware may
delegate this responsibility to a dedicated request handler.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: SHOULD and MAY should be capitalized to mean the defined SHOULD and MAY, I think, since this is in the spec proper.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding is they should only be capitalized when they have the semantic meanings as defined in RFC 2119; the question is whether or not those words here have those semantic meanings. I did not originally write this with those meanings in mind.

The problem, of course, is that there are very few ways to re-word this to have the same intended meaning without using those words. Can you propose an alternative? Or do you feel they have those semantics?

Copy link
Contributor

Choose a reason for hiding this comment

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

If not RFC2119, maybe can could be used for both?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CAN also has semantic meaning via RFC 2119. :)

Been discussing this and the idea of moving the section to the spec document itself heavily today; hopefully we can have resolution shortly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't see CAN mentioned in the RFC... hence me thinking it safe.

Would something wordy, like could potentially, do the job without sounding like a 2119 directive?

from the outside-in, passing each request handler "layer" to the next outer one.

```php
class DecoratedRequestHandler implements RequestHandlerInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: Technically, this isn't a decoratED handler. It's a decoratING handler. The name should be more descriptive. Sorry for not catching that before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

The meta document clearly details that dispatch systems are a NON-GOAL
of the specification, so any guidance we provide around that should be
in the meta document, and not the specification itself.

I have pushed the paragraph back to the section 6.2 pre-amble, with
changes as discussed in Slack.
@weierophinney
Copy link
Contributor Author

At this time, based on discussion with the working group, I've moved the paragraph in question back to the meta document's section 6.2 preamble, with changes as discussed here and elsewhere to remove implications that these are anything more than optional recommendations.

@weierophinney weierophinney merged commit 4b417c9 into php-fig:master Jan 11, 2018
@weierophinney weierophinney deleted the fix/psr-15-meta-corrections branch January 11, 2018 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants