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

Create initial proposal for middleware interface #755

Merged
merged 3 commits into from
Jun 6, 2016
Merged

Create initial proposal for middleware interface #755

merged 3 commits into from
Jun 6, 2016

Conversation

shadowhand
Copy link
Contributor

@sagikazarmark
Copy link
Member

I would rename it to HTTP Middleware as it is closely PSR-7 related, but middlewares themselves are not.

@pmjones
Copy link
Contributor

pmjones commented May 10, 2016

I would think @mindplay-dk has right of first refusal as "editor" here, since he brought it up.

* [Zend Stratigility](https://github.com/zendframework/zend-stratigility/blob/1.0.0/src/MiddlewarePipe.php#L69-L79)
* [Relay](https://github.com/relayphp/Relay.Relay/blob/1.0.0/src/MiddlewareInterface.php#L24)
* [Slim](https://github.com/slimphp/Slim/blob/3.4.0/Slim/MiddlewareAwareTrait.php#L66-L75)
* [Middleman](https://github.com/mindplay-dk/middleman/blob/1.0.0/src/MiddlewareInterface.php#L24)
Copy link
Contributor

Choose a reason for hiding this comment

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

@shadowhand

How about keeping these projects sorted alphabetically?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the additional list!

@shadowhand
Copy link
Contributor Author

@pmjones I'm perfectly happy to let @mindplay-dk take ownership. My goal was simply to get the ball rolling.

@shadowhand
Copy link
Contributor Author

@sagikazarmark I'm not opposed to a rename.

@pmjones
Copy link
Contributor

pmjones commented May 10, 2016

@shadowhand Good form, sir. :-)

@mindplay-dk
Copy link
Contributor

Just dropping a link to the draft I wrote here:

https://gist.github.com/mindplay-dk/1d60ccfa083acfca32698d3c6d9a0945

I'm not going to do much more now - I don't have time. Will check in here and on the mailing list when I can.

@shadowhand feel free to use whatever you want from what I drafted. (That awesome list by @localheinz should be added to it, I think - I started, but only have a few items on mine.)

@pmjones
Copy link
Contributor

pmjones commented May 10, 2016

Very good. @mindplay-dk thanks for bringing up the idea; @shadowhand it looks like you're editor and lead if you want to be. I'll be happy to fill the role of coordinator if you want me to.

@shadowhand
Copy link
Contributor Author

I just pushed a bunch of changes to this, including a complete interface, as well as a definition for the exception and trait for server request checking. Please review and let me know if other changes need to be made before an entrance vote.

@pmjones should I list you as the sponsor of this proposal?

@mindplay-dk I have integrated your draft with my proposal. Some of it was directly copied in and some of it was edited. Please let me know if you don't agree with my changes.

@localheinz the list of middleware has been updated from your list. Thank you for sharing it!

@pmjones
Copy link
Contributor

pmjones commented May 11, 2016

@shadowhand As a sponsor or coordinator, your call.

I think this is a very good start and ready for presentation to the group once a second sponsor is found.

@shadowhand
Copy link
Contributor Author

@MWOP would you be willing to be the second sponsor, since Zend Framework has a clear stake in this proposal?

@localheinz
Copy link
Contributor

@shadowhand

I think you intended to mention @weierophinney, right?!

@shadowhand
Copy link
Contributor Author

@localheinz oops yes I did.

@n1215
Copy link

n1215 commented May 11, 2016

I think RequiresServerRequestTrait::assertServerRequest() should return ServerRequestInterface for code completion.

Maybe another method name like ensureServerRequest is better.

@shadowhand
Copy link
Contributor Author

shadowhand commented May 11, 2016

@n1215 I don't agree. Method chaining increases cognitive load for the sake of convenience and could be abused with immutable objects

EDIT: see https://ocramius.github.io/blog/fluent-interfaces-are-evil/

@n1215
Copy link

n1215 commented May 11, 2016

@shadowhand
This change has no method chain. I cannot understand why fluent interfaces are related to this.
https://gist.github.com/n1215/a3ef3f644bb795dd204c70e730dc3fab

@shadowhand
Copy link
Contributor Author

shadowhand commented May 11, 2016

Returning an object is a fluent interface. There is no need for it.

*
* @throws ServerRequestRequiredException
*/
private function assertServerRequest(RequestInterface $request)
Copy link
Contributor

Choose a reason for hiding this comment

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

this should probably be protected to allow class hierarchies

Copy link
Contributor Author

@shadowhand shadowhand May 12, 2016

Choose a reason for hiding this comment

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

Why would that be necessary? A trait can be used in any scenario and so using protected offers no advantage.

Copy link
Member

Choose a reason for hiding this comment

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

Should this trait be here at all? I mean, for cache there is a util repo. Any non-interface stuff should be placed in such thing and should not be part of the standard IMO.

Copy link
Contributor Author

@shadowhand shadowhand May 12, 2016

Choose a reason for hiding this comment

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

What is the difference between shipping with an optional trait vs having a util repo? Personally I think having a trait is more efficient and easier to use.

Copy link
Member

Choose a reason for hiding this comment

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

IMO it makes it part of the standard, while it isn't. The less content the better it is. And it's an already existing pattern within the FIG.

@mindplay-dk
Copy link
Contributor

IMO the exception and trait are too much - it raises more questions than answers. The exception itself is a runtime-exception anyhow; runtime-exceptions are unchecked, so it doesn't actually matter if these are consistent or not.

I still think that requiring everyone to do a run-time check to see if the incoming request is a server-request, is really problematic - the large majority of middleware will have to implement this check; it seems wrong to favor the most rare case and make every middleware check with instanceof in almost every case.

I would strongly prefer having two interfaces:

  1. ServerMiddlewareInterface accepting ServerRequestInterface as the first argument, and
  2. MiddlewareInterface accepting RequestInterface for the other rare cases

Since the second interface is by far the most rarely used, having to do instanceof check etc. in those few cases is okay - IMO, the primary use case of server-middleware should not have to be littered with run-time type-checks and doc-blocks for IDE support.

@sagikazarmark
Copy link
Member

I agree with @mindplay-dk. Are there any cases when the same middleware can be used for both kinds of messages at all? Because that seems to be the reason behind having one common interface, but I don't think it's a common case, if possible at all.

@shadowhand
Copy link
Contributor Author

The problem with having two different interfaces is in type hinting. At the very minimum, we would end up with three interfaces (one empty):

interface MiddlewareInterface {} // totally empty

ServerMiddlewareInterface extends MiddlewareInterface
{
    public function __invoke(...);
}

ClientMiddlewareInterface extends MiddlewareInterface
{
    public function __invoke(...);
}

The only difference between server and client middleware is the usage of methods that are associated with ServerRequestInterface, such as attributes and cookies.

@pmjones
Copy link
Contributor

pmjones commented May 12, 2016

Agree with @mindplay-dk as well. In addition, Middleware that can be used for both RequestInterface and ServerRequestInterface can be typehinted against just RequestInterface, since (if memory serves) ServerRequestInterface extends it.

@shadowhand
Copy link
Contributor Author

shadowhand commented May 12, 2016

@pmjones sort of, but it doesn't provide protection for ensuring that necessary methods exist, hence the addition of the trait to this proposal.

ServerMiddlewareInterface cannot extend from MiddlewareInterface, because the signatures are different. See here: https://3v4l.org/fVPAV

@sagikazarmark
Copy link
Member

I guess he meant the case when the middleware does not implement the interface, but is a callable.

@pmjones
Copy link
Contributor

pmjones commented May 12, 2016

@shadowhand My point (perhaps mistaken) is that a dispatch mechanism might take any combination of ServerRequest middlewares and (Client)Request middlewares; a middleware used in that dispatch cycle typehinted to (Client)Request can still receive a ServerRequest object and fullfill the typehint. (Perhaps I am forgetting something.)

@shadowhand
Copy link
Contributor Author

shadowhand commented May 12, 2016

@pmjones that's correct, but how the middleware check that the request is of the correct type? There needs to be some mechanism for that and I feel this proposal is incomplete without (at minimum) a standard exception class that can be caught.

@pmjones
Copy link
Contributor

pmjones commented May 12, 2016

how the middleware check that the request is of the correct type

(/me ponders)

I think for a ServerMiddleware typehinted to ServerRequestInterface, obviously no need to check for type, since it's the extended type.

And I think for a ClientMiddleware typehinted to RequestInterface, there's no expectation of the ServerRequest methods, so there's no need to check for type there either.

So for those who want to write a middleware that works for both Request and ServerRequest, depending on only the RequestInterface seems like the right thing to do.

I feel like I'm missing an important part of your question somehow.

@pmjones
Copy link
Contributor

pmjones commented May 12, 2016

p.s. I'm coming at this from the direction of wanting two middleware interfaces, one for client requests and one for server requests. Perhaps that's the disconnect.

@pmjones
Copy link
Contributor

pmjones commented Jun 6, 2016

@svpernova09 svpernova09 merged commit 67c8c42 into php-fig:master Jun 6, 2016
@svpernova09
Copy link
Contributor

I don't have the ability to deploy at the moment. @reinink could you do a site update at your convenience?

@reinink
Copy link
Contributor

reinink commented Jun 6, 2016

@svpernova09 Will do!

@mindplay-dk
Copy link
Contributor

@shadowhand two issues.

Regarding FrameInterface, what's the story behind the term "frame"? A "frame" object, as far as I understand, is just a delegate function for dispatching the next middleware on the stack. I don't understand what the term "frame" is supposed to communicate. I personally refer to the $next function as a "middleware delegate" in daily speech, and would have likely named it something like MiddlewareDelegateInterface, which is a bit wordy, but should raise fewer questions than "frame".

Also, I wonder, with StackInterface, do we have another case of SRP violation? IMO, from the consumer's perspecive, what makes something a "middleware stack", is the ability to process a request.

How you add or remove middleware doesn't affect my ability to dispatch a request - that's really about manipulating an internal array/stack, which is not how I implement immutability in my dispatcher, which is fully-immutable once instantiated; if one wants to manipulate the array/stack of middleware components, one would need to do that prior to instantiation, which is simpler.

Also, my dispatcher creates/initializes middleware on the fly, so that interface wouldn't even make sense or work for me, and I'm planning some other changes that would clash with this even more.

This makes me think that this part of the interface is first of all not necessary, but secondly, doesn't help interoperability - instead, it feels opinionated, and I feel it's actually standing in the way of interop. I realize it's listed as optional, but an optional interface doesn't guarantee interop, so it doesn't even really work for that.

In addition, the ability to process a request probably shouldn't be optional? It seems like this an interface any dispatcher could implement, and it probably should be a MUST, not a MAY, to maximize interop. Which again points at an SRP issue, because, as explain, those other two stack manipulation features are not a good fit for my dispatcher...

@shadowhand
Copy link
Contributor Author

@mindplay-dk I would refer to https://groups.google.com/forum/#!topic/php-fig/GTm9ho6rFts which is covering the exact same things.

@mnapoli
Copy link
Member

mnapoli commented Aug 15, 2016

👍 and https://groups.google.com/forum/#!topic/php-fig/V12AAcT_SxE regarding FrameInterface

@mindplay-dk
Copy link
Contributor

@shadowhand @mnapoli I read both of threads, and I don't feel like either of them addresses either of the issues I'm highlighting.

  1. Separation of dispatch from configuration methods: SRP. My dispatcher won't allow modification - if I modify the stack, I do so prior to constructing the dispatcher. The current interface dictates a specific design, which I don't agree with; it's opinionated.
  2. The ability to process a request should be a MUST, not a MAY - my impression is that it is currently a MAY because of the responsibility overlap with configuration, which needs to be optional; I believe this points at two distinct responsibilities?
  3. No one suggested "delegate" as the proper term in those discussions? - I've posted the suggestion to that thread for further discussion.

@mnapoli
Copy link
Member

mnapoli commented Aug 16, 2016

Separation of dispatch from configuration methods: SRP. My dispatcher won't allow modification - if I modify the stack, I do so prior to constructing the dispatcher. The current interface dictates a specific design, which I don't agree with; it's opinionated.

Agreed.

The ability to process a request should be a MUST, not a MAY - my impression is that it is currently a MAY because of the responsibility overlap with configuration, which needs to be optional; I believe this points at two distinct responsibilities?

Agreed.

No one suggested "delegate" as the proper term in those discussions? - I've posted the suggestion to that thread for further discussion.

That's already an improvement, even though still vague but that's just my opinion.

While I agree with your remarks about StackInterface my point is that interface should not be in the standard at all, hence those problems disappear.

@mindplay-dk
Copy link
Contributor

mindplay-dk commented Aug 16, 2016

my point is that interface should not be in the standard at all, hence those problems disappear

Note that I'm not making that assertion - I'm not opposing the interface, as long as it's optional, and separate from the interface that makes it dispatchable.

The existence of a middleware delegate interface does seem to have some merit in terms of offline inspections and IDE support, do you not agree?

I'm currently implementing a different type of middleware stack, not for HTTP requests, and I'm using the approach with type-safe delegate - I will say that, in any other regard besides inspections/IDE support, those delegates are themselves merely dumb delegates to internally-generated closures, e.g. something like:

class MiddlewareDelegate implements MiddlewareDelegateInterface
{
    /**
     * @var callable
     */
    private $delegate;

    /**
     * @param callable $delegate
     */
    public function __construct(callable $delegate)
    {
        $this->delegate = $delegate;
    }

    /**
     * @param Request $request
     *
     * @return Response
     */
    public function resolve(Request $request)
    {
        return call_user_func($this->delegate, $request);
    }
}

In other words, this doesn't appear to have any value beyond external type-safety - internally, it's still call_user_func() with no safety.

I am somewhat concerned about the performance implications - in micro benchmarks, these dispatchers are going to measurably slower than simpler dispatchers using callables with no object-wrapping. In practice, probably only measurable in micro benchmarks though; in practice, the cost is negligible for sure.

@mnapoli
Copy link
Member

mnapoli commented Aug 16, 2016

The existence of a middleware delegate interface does seem to have some merit in terms of offline inspections and IDE support, do you not agree?

Do you have an example? I really don't see it. Here is something concrete: my implementation of StackInterface: https://github.com/stratifyphp/http/blob/master/src/Middleware/Pipe.php How would another interface on top of that class improve anything? (note it's implementing Middleware)

@mindplay-dk
Copy link
Contributor

@mnapoli callable $next is where this would help - you don't have static analysis or IDE support for calls to $next now, e.g. Pipe.php#49: return $next($request, $response)

By changing this into e.g. MiddlewareDelegateInterface $next, you'd get static analysis and IDE auto-complete for a more concise statement like return $next->process($request, $response).

Even for those who don't use static analysis tools or an IDE, you'd get the benefit of a bad call failing immediately at the call site, as opposed to failing later, or (worse) executing without error and producing a bad result - that sort of thing is much harder to debug.

@mnapoli
Copy link
Member

mnapoli commented Aug 16, 2016

Oh OK you are talking about FrameInterface, I mentioned StackInterface so here's the confusion :)

In the thread I linked there was this proposal: https://groups.google.com/d/msg/php-fig/V12AAcT_SxE/ZrNIz46zCAAJ What do you think about it? It's a middleground between simplicity ($next($request) is so simple it would be too bad to complicate it just even a little) and type safety:

interface Delegate
{
    public function __invoke(ServerRequestInterface $request) : ResponseInterface;
}

Can be then used like this:

function (ServerRequestInterface $request, Delegate $next) {
    return $next($request);
}

WDYT?

@mindplay-dk
Copy link
Contributor

mindplay-dk commented Aug 16, 2016

@mnapoli the problem with __invoke() is you can't really type-hint against it, or at least only with php-doc, if that means you expect a callable to also pass as an argument... whereas, if you do expect to statically type-hint, and don't expect a callable to work, using __invoke() serves no purpose - and semantically, $next refers to the next middleware, so $next->process() to me reads as telling the next middleware to process, whereas $next(), hmm, doesn't mean much...

Also, some people have voiced concern that reserving __invoke() in this manner prevents you from using __invoke() for your own purposes. I don't so much see the use-case, but either way, I think a "real" interface with a real method is more idiomatic and conventional than __invoke() magic.

@mindplay-dk
Copy link
Contributor

mindplay-dk commented Aug 16, 2016

On a side note, an interface like this serves more than one purpose:

interface DispatchableInterface
{
    public function dispatch(RequestInterface $request): ResponseInterface;
}

That is, a middleware stack could implement it - or a simple controller could implement it; which means you could use, in some contexts, a simple controller or an entire middleware stack interchangibly, which could create interesting opportunities, as this could work for a bunch of different types of components.

I would not name it StackInterface, since that implies a singular purpose, rather than describing a general facet implemented by dispatchers, controllers, decorators of either, and possibly many other components.

@mnapoli
Copy link
Member

mnapoli commented Aug 16, 2016

if you do expect to statically type-hint, and don't expect a callable to work, using __invoke() serves no purpose

It's simpler and it's very similar to current middlewares: return $next($request). Developers will be familiar with that. With FrameInterface not so much.

whereas $next(), hmm, doesn't mean much...

But… that's what PSR-7 middlewares are doing today: when has it become bad/complex? Everybody uses it and it works so well exactly because it's simple.

We don't need to have names for everything. We struggle finding a name for that thing because it's nothing really, it's just an action: "call the next middleware", there's nothing more to it. We have a solution today, it works, it's simple, let's try to keep its simplicity.

Trying to force it into a concept of a frame or a delegate or something is just adding complexity for no gain. If we reaaaally want type safety (I see no real added value because any good middleware framework will type-hint correctly the $next callable anyway) __invoke() solves the problem.

reserving __invoke() in this manner prevents you from using __invoke() for your own purposes

That's fine. Will there really be an implementation of the "delegate/frame" that needs to be invokable for other reasons? Do we have actual examples of that? (not just theoretical scenarios) And if so, can the project write an adapter?
The delegate object will usually be a very simple one, it will not have a lot of responsibilities (judging from existing implementation).

I would not name it StackInterface, since that implies a singular purpose

I think you are confusing Frame and Stack which are 2 different things. Or are you switching topic?

@sagikazarmark
Copy link
Member

But… that's what PSR-7 middlewares are doing today

I've heard this argument quite a few times (or in a more general form: everyone is doing that, how can it be wrong?). I don't think it's a valid argument. Someone started it, some other people continued the "standard" and that's why it became so popular. It doesn't mean it's good (or bad), but again: it doesn't make it a valid argument.

Trying to force it into a concept of a frame or a delegate or something is just adding complexity for no gain.

The concept of delegates exist in other languages and I am very happy when I can use a language where it exists. The fact that it doesn't exist in PHP shouldn't keep us from "emulating" the idea. If PHP receives delegates in a future version, it will be easier to adapt.

Will there really be an implementation of the "delegate/frame" that needs to be invokable for other reasons?

No, it's probably bad design, but I still agree with @mindplay-dk. For me using __invoke in an interface is weird. I had the same feeling about __toString in PSR-7.

@mnapoli
Copy link
Member

mnapoli commented Aug 17, 2016

👍 I don't have much to add so I'll avoid going circles. Just about this:

I've heard this argument quite a few times (or in a more general form: everyone is doing that, how can it be wrong?). I don't think it's a valid argument.

A standard is not a place for innovation (I personally learnt this with ContainerInterface which was massively rejected years ago). What if we standardize something that sucks (whatever the reason)?

On the other hand past experience is very useful:

  • did it work?
  • was there any problems with it? (I'm happy that $response was removed from the signature for example)

I've never seen $next as a problem that needed solving. The arguments I see here are only theoretical and IMO reality is much more important than theory.

@sagikazarmark
Copy link
Member

What if we standardize something that sucks (whatever the reason)?

Although there is a chance for that (see PSR-6), I don't think that could happen because there is no better solution, rather because the development process sucks.

The fact that there are no problems with $next is also not a valid argument against doing something else IMO. There were arguments however why other solutions would be better.

@mindplay-dk
Copy link
Contributor

@mnapoli

The arguments I see here are only theoretical and IMO reality is much more important than theory

in my opinion, using an idiomatic interface/method does have an advantage: you can type-hint it for static analysis and IDE support. And it will fail early, which makes for easier debugging. Both of those things are practical, not theoretical.

The only advantage of __invoke() magic over a real interface, is it's familiar to middleware authors - but so are regular interfaces, and not just in the context of middleware, but in PHP in general, which is why I say it's more idiomatic.

Sure, a callable is less of a surprise to those already familiar with de-facto PSR-7 middleware, but this standard is already something else entirely - and to everyone else, the callable requires an explanation, whereas an interface is the explanation.

A standard is not a place for innovation

What could be less innovative that a plain old interface?

A standard should be more than a formalization of the familiar - if we can do something better/cleaner and fix mistakes of the past, now is the only opportunity to do that. We've already (most of us) crossed the bridge from double-pass to lambda-style, which was an important step, and a massive break from the de-facto standard, which will benefit the community in the long run.

@mnapoli
Copy link
Member

mnapoli commented Aug 17, 2016

@sagikazarmark I'm not against improvements, but the only way to be sure something is stable/good enough is to try it out in real scenarios. If we innovate, we must then take some time (months/years?) to try that out, else (yes) we risk PSR-6 again. Even PSR-7 has issues that we only discovered when actually using it widely.

@mindplay-dk

you can type-hint it for static analysis and IDE support

I agree with that.

And it will fail early, which makes for easier debugging

I don't agree with this: all stack/pipe implementations I've seen type-hint properly the arguments so it will fail early (https://github.com/zendframework/zend-stratigility/blob/master/src/Next.php#L78, https://github.com/slimphp/Slim/blob/3.x/Slim/MiddlewareAwareTrait.php#L66) if you pass something wrong.

Sure, a callable is less of a surprise to those already familiar with de-facto PSR-7 middleware, but this standard is already something else entirely - and to everyone else, the callable requires an explanation, whereas an interface is the explanation.

I could agree with that if FrameInterface wasn't the root of the problem :) We cannot succeed (yet) to make that interface the explanation because it's confusing.

What could be less innovative that a plain old interface?

By that logic it's impossible to do something new unless we add new operators/concepts to the language.

@weierophinney
Copy link
Contributor

@mnapoli I've commented on this extensively on the list, but I'll summarize here:

  • Making it a pure callable makes it potentially incompatible with existing stacks (particularly if they are currently double pass), which would hamper adoption by existing libraries.
  • Defining __invoke() as the interface method similarly hampers adoption when libraries already define this, as many would have to change in backwards incompatible ways in order to accommodate existing middleware as well as middleware targeting this specification. Having a discrete, unique method ensures these can interoperate immediately, and allows tools to be built to help users migrate their middleware to work with the specification.

@mnapoli
Copy link
Member

mnapoli commented Aug 17, 2016

@weierophinney Just to be sure: are you talking about FrameInterface?

@weierophinney
Copy link
Contributor

@mnapoli Yes, the current FrameInterface.

mnapoli added a commit that referenced this pull request Aug 17, 2016
@mnapoli
Copy link
Member

mnapoli commented Aug 17, 2016

@weierophinney your response in the mailing list thread was great, I wasn't expecting existing middlewares to work with PSR-15 stacks (and the other way around), so I had no idea you wanted to make the 2 systems work together at that level.

I've opened a PR to document all that: #807 Reviews are welcome.

@weierophinney
Copy link
Contributor

I wasn't expecting existing middlewares to work with PSR-15 stacks (and the other way around)

I'd been wanting invokables as well, but when I started considering what it would take to make the transition within Expressive or Slim, I realized that doing so made migration substantially harder. Having just come off of Zend Framework v2 -> v3 transitions, I recognized the same challenges we had there, as well as the same opportunities to make the transition easier for both library authors and consumers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.