Create initial proposal for middleware interface #755

Merged
merged 3 commits into from Jun 6, 2016

Projects

None yet
@shadowhand
Contributor
@hannesvdvreken hannesvdvreken referenced this pull request in php-http/http-kernel May 10, 2016
Open

A year passed... #1

@sagikazarmark
Contributor

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

@pmjones
Contributor
pmjones commented May 10, 2016

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

@localheinz localheinz and 1 other commented on an outdated diff May 10, 2016
proposed/middleware-meta.md
+
+* The middleware is defined as a [callable][php-callable].
+* The middleware is passed 3 arguments during invocation:
+ 1. A `ServerRequestInterface` implementation.
+ 2. A `ResponseInterface` implemenation.
+ 3. A `callable` that receives the request and response and executes the next middleware.
+* The implementation of the middleware manager is not specified as an interface.
+
+[php-callable]: http://php.net/manual/language.types.callable.php
+
+A similar interface definition can be seen in the following frameworks:
+
+* [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)
@localheinz
localheinz May 10, 2016 Contributor

@shadowhand

How about keeping these projects sorted alphabetically?

@shadowhand
shadowhand May 10, 2016 Contributor

Thanks for the additional list!

@shadowhand
Contributor

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

@shadowhand
Contributor

@sagikazarmark I'm not opposed to a rename.

@pmjones
Contributor
pmjones commented May 10, 2016

@shadowhand Good form, sir. :-)

@mindplay-dk
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
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
Contributor

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
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
Contributor

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

@localheinz
Contributor

@shadowhand

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

@shadowhand
Contributor

@localheinz oops yes I did.

@n1215
n1215 commented May 11, 2016

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

Maybe another method name like ensureServerRequest is better.

@shadowhand
Contributor
shadowhand commented May 11, 2016 edited

@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
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
Contributor
shadowhand commented May 11, 2016 edited

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

@shadowhand shadowhand referenced this pull request in php-cds/php-cds May 12, 2016
Closed

[RFC] Middleware Interface #5

@mindplay-dk mindplay-dk and 2 others commented on an outdated diff May 12, 2016
proposed/middleware.md
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ServerRequestInterface;
+
+trait RequiresServerRequestTrait
+{
+ /**
+ * Check that the given request is a server request.
+ *
+ * @param RequestInterface $request
+ *
+ * @return void
+ *
+ * @throws ServerRequestRequiredException
+ */
+ private function assertServerRequest(RequestInterface $request)
@mindplay-dk
mindplay-dk May 12, 2016 Contributor

this should probably be protected to allow class hierarchies

@shadowhand
shadowhand May 12, 2016 edited Contributor

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

@sagikazarmark
sagikazarmark May 12, 2016 Contributor

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.

@shadowhand
shadowhand May 12, 2016 edited Contributor

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.

@sagikazarmark
sagikazarmark May 12, 2016 Contributor

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 mindplay-dk and 2 others commented on an outdated diff May 12, 2016
proposed/middleware.md
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+interface MiddlewareInterface
+{
+ /**
+ * Process a request and return a response.
+ *
+ * Takes the incoming request and optionally modifies it before delegating
+ * to the next handler to get a response. May modify the response before
+ * ultimately returning it.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @param callable $next
@mindplay-dk
mindplay-dk May 12, 2016 Contributor

needs inline documentation explaining what this is - a lot of people (myself included) thought this was the next middleware component on the stack, which it isn't, it's a delegate with a different signature.

I would suggest using adding an interface for the delegate type-hint this parameter as @param callable|MiddlewareDelegateInterface $next - or otherwise, add inline documentation displaying the method signature, e.g.:

@param callable|MiddlewareDelegateInterface $next delegate function to dispatch the next middleware component:
                                                  function (RequestInterface $request, ResponseInterface $response) : ResponseInterface
@samdark
samdark May 12, 2016 Contributor

Agree. I've thought it's next middleware as well at first.

@shadowhand
shadowhand May 12, 2016 Contributor

@mindplay-dk the meta clearly states that defining the dispatch mechanism is not a goal. I'm happy to document it more, but I don't think adding an interface is appropriate for the scope of this proposal.

@mindplay-dk
mindplay-dk May 14, 2016 Contributor

This isn't about the dispatch mechanism, it's just a matter of missing documentation for the $callable argument - it's not obvious to anyone what exactly that callable is, most people seem to think it's the next middleware component, which isn't true, it's a delegate to dispatch the next middleware function.

Since we're not defining the delegate interface as an actual type, we do need to document what it is:

@param callable $next delegate function to dispatch the next middleware component:
                      function (RequestInterface $request, ResponseInterface $response): ResponseInterface

Yes?

@mindplay-dk
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
Contributor

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
Contributor

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
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
Contributor
shadowhand commented May 12, 2016 edited

@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
Contributor

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

@pmjones
Contributor
pmjones commented May 12, 2016 edited

@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
Contributor
shadowhand commented May 12, 2016 edited

@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
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
Contributor
pmjones commented May 12, 2016 edited

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.

@shadowhand
Contributor

There's no problem with a middleware that only needs the methods in Request. The problem is when you do require ServerRequest methods you either have to:

  1. Typehint against RequestInterface, which could result in a fatal error the request is not ServerRequestInterface.
  2. Implement a totally different interface in order to type hint against ServerRequestInterface.

I was attempting to solve the former relatively cleanly by providing a trait that components could use.

The problem with the latter is that validating middleware type requires checking two entirely different interfaces, rather than one. Which means you cannot type hint against it in your dispatcher.

@mindplay-dk
Contributor

As far as I can figure, we only need two interfaces.

Per PSR-7 there are two levels, not two kinds of requests - a general "request" and a specialized "server request".

Our middleware architecture should mirror that, e.g. a general "middleware" and a specialized "server middleware".

Though I can't quite figure out if or how that's possible with PHP... it seems to require either a type union or generics or some other feature PHP doesn't seem to have... hmm...

@shadowhand
Contributor

I'm strongly opposed to the two interface solution but would be open to a three interface solution. Two interfaces means that no type hints can be used for dispatch/collection, because the interfaces have no common ancestor. Three interfaces (one empty) allows a small amount of assurance that you're getting what you want.

@mindplay-dk
Contributor

I suppose, if we can't have two interfaces (in a meaningful way) then middleware will need to be implemented something like:

class MyServerMiddleware implements MiddlewareInterface {
    public function __invoke(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) : ResponseInterface {
        return $this->process($request, $response, $next);
    }

    private function process(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) : ResponseInterface {
        // ...
    }
}

That seems really shite. I think IDEs and static analysis tools will warn for the call in __invoke() right off the bat. hmmm....

@mindplay-dk
Contributor
mindplay-dk commented May 12, 2016 edited

Or maybe we'll have to ship it with a base class for server middleware?

abstract class ServerMiddleware implements MiddlewareInterface
{
    final public function __invoke(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) : ResponseInterface {
        if (! $request instanceof ServerRequestInterface)) {
            throw new RuntimeException("middleware implementation error :-(");
        }

        return $this->run($request, $response, $next);
    }

    abstract public function run(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) : ResponseInterface;
}
@shadowhand
Contributor

@mindplay-dk I really don't like that.

@mindplay-dk
Contributor

IMO, an empty interface is an ugly work-around and it's no assurance of anything - it could lead to confusion as well, what if somebody implements the empty interface? It'll pass the type-check even if it has neither the server or client signature.

Well, heck...

interface MiddlewareInterface
{
    public function __invoke(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    );
}

/**
 * @method __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
 */
interface ServerMiddlewareInterface extends MiddlewareInterface
{
}

Hahaha ;-)

@shadowhand
Contributor

If someone implements an empty interface it will blow up in their face. I'm fine with that.

@mindplay-dk
Contributor

Well, since there's no happy middle ground with PHP as it is today, I guess we'll have to settle for basically what the community is already doing - have a middleware-interface, and doc-block your implementation of __invoke() with @param ServerRequestInterface $request and deal with the missing type-check.

@mindplay-dk
Contributor

Or check with instanceof ServerRequestInterface and throw as needed.

I think we should leave it alone and let it be - there's no good solution.

@shadowhand
Contributor

Three interface solution is ugly but there is precendent with Traversable.

@mindplay-dk
Contributor

@shadowhand well, maybe, but for middleware there is precedent for one interface.

@mindplay-dk
Contributor

There are obviously real reasons why the community at large already settled on the one interface - I don't think there's any point in us wrecking our heads to come up with a more complex way to overthrow that.

I propose we stick with the one interface, and documentation showing how to implement and doc-block a server-only middleware component.

@shadowhand
Contributor

In which case, the current implementation is ideal because it provides a standard mechanism for checking for the correct request type.

@pmjones
Contributor
pmjones commented May 12, 2016

Well, I think I'm still interested in seeing two interfaces, but not enough to block the proposal as it stands. I reserve the right to bring it back up at a later time. :-)

@shadowhand
Contributor

We still need one more sponsor before an entrance vote can be started.

@mindplay-dk
Contributor

In which case, the current implementation is ideal because it provides a standard mechanism for checking for the correct request type

I still think this is unnecessary complexity - I think we should formalize the interface and stay away from implementation. The implementation part is not essential to interoperability. IMO, it's an unnecessary (though minor) burden on maintainers of pre-existing middleware.

I'd like us to think of this PSR as naming and describing the already de-facto middleware standard, and formalizing the interface.

A future PSR can deal with anything else if/when PHP allows for something better.

@mindplay-dk
Contributor

naming and describing the already de-facto middleware standard and formalizing the interface

Perhaps this is what the first section of the PSR should state?

@shadowhand
Contributor

Alright, then I will update this PR accordingly sometime in the next 48 hours.

@shadowhand
Contributor

@mindplay-dk @pmjones I have removed everything but the single interface, added some additional documentation for the $next parameter, and clarified the intent to create standard from existing implementations.

Hopefully this latest update is sufficient to start an entrance vote.

@pmjones
Contributor
pmjones commented May 13, 2016

We need two sponsors for the entrance vote. I'm happy to be noted as coordinator under Sponsors, or as just a sponsor, which means we need at least one more. (Not offended if you don't want me as a sponsor, FWIW.)

@shadowhand shadowhand and 1 other commented on an outdated diff May 14, 2016
proposed/middleware.md
+ * @param ResponseInterface $response
+ * @param callable $next
+ *
+ * @return ResponseInterface
+ */
+ public function __invoke(
+ RequestInterface $request,
+ ResponseInterface $response,
+ callable $next
+ );
+}
+```
+
+The callable `$next` refers to a middleware dispatcher that will pass the given
+request and response instances to the next middleware in the queue. It MUST NOT
+be a middleware component.
@shadowhand
shadowhand May 14, 2016 Contributor

@pmjones @mindplay-dk I documented the $next details here, but it sounds like I need to put it in the interface itself?

I'm starting to think that we should define a MiddlewareDispatcherInterface just to provide clarity.

@shadowhand
shadowhand May 14, 2016 Contributor

For reference, Relay\Runner does it quite elegantly and this signature could easily be made into a general interface.

@mindplay-dk
mindplay-dk May 14, 2016 Contributor

I documented the $next details here, but it sounds like I need to put it in the interface itself?

Yes, please - inline documentation e.g. like here.

For reference, Relay\Runner does it quite elegantly and this signature could easily be made into a general interface

I thought we agreed that standardizing on the dispatcher interface is pointless?

We shouldn't do it just because we can.

@shadowhand shadowhand commented on an outdated diff May 14, 2016
proposed/middleware.md
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+interface MiddlewareInterface
+{
+ /**
+ * Process a request and return a response.
+ *
+ * Takes the incoming request and optionally modifies it before delegating
+ * to the next handler to get a response. May modify the response before
+ * ultimately returning it.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @param callable $next delegate function that will dispatch the next middleware component:
+ * function (RequestInterface $request, ResponseInterface $response): ResponseInterface
@shadowhand
shadowhand May 14, 2016 Contributor

@mindplay-dk @pmjones added documentation for $next to the interface.

@shadowhand shadowhand and 1 other commented on an outdated diff May 14, 2016
proposed/middleware.md
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @param callable $next delegate function that will dispatch the next middleware component:
+ * function (RequestInterface $request, ResponseInterface $response): ResponseInterface
+ *
+ * @return ResponseInterface
+ */
+ public function __invoke(
+ RequestInterface $request,
+ ResponseInterface $response,
+ callable $next
+ );
+}
+```
+
+Note that the callable `$next` MUST NOT be a middleware component.
@shadowhand
shadowhand May 14, 2016 Contributor

I felt having this note was still important to anyone reading the document.

@mindplay-dk
mindplay-dk May 14, 2016 Contributor

Yes, very good - pencil it out :-)

@mindplay-dk
mindplay-dk May 14, 2016 Contributor

In fact, I would elaborate a little bit:

Note that a dispatcher MUST NOT pass an actual middleware component as the
callable `$next` - this callable is not the actual middleware component, but
a *delegate* function to dispatch the next middleware component, generated by
the dispatcher.
@shadowhand
shadowhand May 17, 2016 Contributor

@mindplay-dk I've updated this bit of documentation based on your recommendation.

@mindplay-dk mindplay-dk and 2 others commented on an outdated diff May 17, 2016
proposed/middleware.md
+ * function (RequestInterface $request, ResponseInterface $response): ResponseInterface
+ *
+ * @return ResponseInterface
+ */
+ public function __invoke(
+ RequestInterface $request,
+ ResponseInterface $response,
+ callable $next
+ );
+}
+```
+
+Note that a dispatcher MUST NOT pass an actual middleware component as the
+callable `$next` - this callable is not the actual middleware component, but
+a *delegate* function to dispatch the next middleware component, generated by
+the dispatcher. A simple implementation MAY be defined as:
@mindplay-dk
mindplay-dk May 17, 2016 Contributor

I would change this from MAY be defined to could be defined (lower case could) since this is merely a suggestion/example, and not a rule or recommendation pertaining to the RFC per se.

@elazar
elazar May 17, 2016

This usage of "MAY" is consistent with the prescribed vocabulary detailed in RFC 2119.

  1. MAY This word, or the adjective "OPTIONAL", mean that an item is
    truly optional. One vendor may choose to include the item because a
    particular marketplace requires it or because the vendor feels that
    it enhances the product while another vendor may omit the same item.
    An implementation which does not include a particular option MUST be
    prepared to interoperate with another implementation which does
    include the option, though perhaps with reduced functionality. In the
    same vein an implementation which does include a particular option
    MUST be prepared to interoperate with another implementation which
    does not include the option (except, of course, for the feature the
    option provides.)
@shadowhand
shadowhand May 17, 2016 Contributor

@mindplay-dk at this point I think the language is fine for an entrance vote. Additional changes may be made during the DRAFT stage of this PSR.

@shadowhand
Contributor

@pmjones a second sponsor has been identified and the meta document updated.

@bradynpoulsen bradynpoulsen and 1 other commented on an outdated diff May 17, 2016
proposed/middleware-meta.md
+* Provide a middleware interface that is compatible with PSR-7 as a Composer package.
+* Implement an already widely-adopted informal standard.
+
+## 3.2 Non-Goals
+
+* Attempting to define the mechanism by which middleware is managed or dispatched.
+
+4. Approaches
+-------------
+
+### 4.1 Chosen Approach
+
+Based on the middleware implementations already used by frameworks that have
+adopted PSR-7, the following commonalities are observed:
+
+* The middleware is defined as a [callable][php-callable] using `__invoke`.
@bradynpoulsen
bradynpoulsen May 17, 2016

Too bad that this RFC is still under discussion so middleware wouldn't need to be specifically tied to __invoke
https://wiki.php.net/rfc/callable-types

@shadowhand
shadowhand May 17, 2016 Contributor

That RFC won't help people that are still on PHP 5.

@bradynpoulsen
bradynpoulsen May 17, 2016 edited

True, but how long will PSRs be written towards PHP 5? I meant in a future scope sense. It would be interesting to have *.0 of PSRs be written for PHP 5+, *.1 for PHP 7+, and *.2 for PHP 7.1+. Certainly a discussion outside the scope of this PSR though :)

@shadowhand
shadowhand May 22, 2016 Contributor

I'd rather a sequential version of the implemented interface be created that requires PHP 7. Version 1.0 should target PHP 5+. Version 2.0 could be PHP 7+.

@pmjones
Contributor
pmjones commented May 17, 2016

Working on it now.

@vlakoff
vlakoff commented May 21, 2016

Just throwing it out there: All About Middleware.

@shadowhand
Contributor

@vlakoff you forgot to include the rebuttal: http://shadowhand.me/all-about-psr-7-middleware/

@vlakoff
vlakoff commented May 22, 2016

Thanks. I just wasn't aware of it :-)

@frederikbosch
frederikbosch commented May 22, 2016 edited

While following the debate I came to the understanding that the current proposal has quite some implementations already. Proof of concept is a good thing for a standard! However, I also feel for the technical concerns brought by @ircmaxell. I think we should not ignore that before accepting the standard.

So I asked myself the question: isn't there a third option? And that same day while doing research for a project I stumbled upon the middleware solution by the iron framework (HTTP framework for rust). They have decided not create a single middleware interface but four. I would like to share this methodology by translating these four interfaces into what could be a psr. I want to suggest to take this method into consideration. The four interfaces are.

  1. BeforeMiddlewareInterface
  2. AfterMiddlewareInterface
  3. AroundMiddlewareInterface
  4. HandlerInterface

Why four interfaces? Because they all have different responsibilities within the chain. I am quoting the iron framework here.

A request first travels through all BeforeMiddleware, then a Response is generated by the Handler, which can be an arbitrary nesting of AroundMiddleware, then all AfterMiddleware are called with both the Request and Response. After all AfterMiddleware have been fired, the response is written back to the client.

[b] = BeforeMiddleware
[a] = AfterMiddleware
[[h]] = AroundMiddleware
[h] = Handler

With no errors, the flow looks like:

[b] -> [b] -> [b] -> [[[[h]]]] -> [a] -> [a] -> [a] -> [a]

What are the advantages of four interfaces over a single one?

  1. Explicitness by clear separation of concerns. A middleware that is there to modify a request (before) does not have a response parameter.
  2. Readability. You do not have to lookup the body of the __invoke method or the handle method of the implementation to see what task the middleware is doing. The class signature tells you which tasks are being done.
  3. Middlewares can implement multiple interfaces because the signatures of the interfaces do not collide with each other.
  4. No discussion on parameters
  5. No discussion on the name of the methods
  6. The PSR standardizes the http chain, not a method.

Hopefully this adds to the discussion. As standardization body we should pursue to involve a large group of the community while creating technical superior solutions.

@pmjones
Contributor
pmjones commented May 22, 2016

I think we should not ignore that before accepting the standard.

FWIW this proposal is not in an acceptance phase; it's in an entrance phase. There's a long road ahead to acceptance.

@shadowhand
Contributor
shadowhand commented May 22, 2016 edited

@frederikbosch this is actually an interesting idea to me, more so than any other alternative that has been proposed. But since PHP doesn't have multiple return types, what would the signatures for each interface look like? I imagine before and after are easy:

before(request): request
after(request, response): response

But I'm struggling to infer what around() would look like.

@mindplay-dk
Contributor

IMO the four interfaces idea just formalizes four patterns that can just as well be implemented informally with the existing proposal. It's adding complexity by formalizing four concepts that are probably more readily understood by simply reading/writing code. It also creates a kind of lock-in, where changing the behavior of a middleware component becomes a breaking change, due to the change in type.

I see this as a misguided attempt to clarify patterns that are possible with middleware, but resulting in more mental overhead, learning curve and overall complexity due to naming and formally implementing those patterns.

In my opinion, this is unnecessary and adds no real value.

@mindplay-dk
Contributor

Side note: if a framework wishes to formalize and implement those four interfaces, that doesn't create any conflict or issue - the framework can absolutely implement this and still comply with the standard.

In my opinion, this is an architectural detail - it's opinionated, and the PSR itself should not dictate architecture.

@frederikbosch

@pmjones Thanks for clearing the official status. I was not unaware that we were not past entering phase. No time to rush then!

@mindplay-dk Thanks for your feedback. I appreciate that you have taken the time to actually review my suggestion. I am not grateful that you say that my proposal is misguiding. Nobody is trying to misguide anyone here. Try to use your words more carefully in that sense.

@shadowhand I am happy that your initial response is positive. You were not the only one. @ircmaxell told me to prefer a single interface but he has nothing against this method. While I am not trying to be a mediator here, I do hope we can contribute together in creating a final solution.

Then technically speaking. @mindplay-dk brought up valid concerns that the four interfaces dictate architecture. I can agree with that. Especially, the handler and around interface are not required for a standard.

So that leaves two interfaces. BeforeInterface and AfterInterface, both containing a single method: before and after. I think that is not complex but simple. And the advantages that I put forward in my earlier message are still valid.

@mindplay-dk
Contributor

@frederikbosch I said "misguided", not "misguiding" - I'm not trying to imply you're trying to misguide anybody, nothing personal was implied with that statement. I'm merely saying your motivation doesn't seem to come from a goal that I agree with.

My position on two interfaces is the same as on four interfaces: one interface is enough. While common terms like "before" and "after" are immediately easier to relate to, it still adds mental overhead: before or after what?

In my opinion, code is perfectly capable of expressing before/after/around data flows - it's been more than sufficient up until now, I don't know why we need to complicate things by introducing more concepts when a single concept already covers all the requirements.

In my opinion, even dictating a small amount of architecture that isn't strictly necessary to accomplish a function, is unnecessary complexity.

And again, if some framework wants to implement those two interfaces (or more) they can do so and remain compatible.

@frederikbosch

@mindplay-dk Thanks again for sending your reply, and clearing up your statement.

We are in disagreement. You think that a single interface covers all the requirements, while the discussion between @shadowhand and @ircmaxell has shown clearly that the exact design of the interface is not that simple. The design that I am pushing removes the discussion around the design by splitting the interface into two methods. Let's see what others are thinking!

@tuupola
tuupola commented May 23, 2016 edited

We are in disagreement. You think that a single interface covers all the requirements, while the discussion between @shadowhand and @ircmaxell has shown clearly that the exact design of the interface is not that simple.

Despite the long blog post by @ircmaxell, it describes only one technical problem which is actually quite simple. Passing the $response as parameter enables those who do not know what they are doing write bad code. This is because it enables modifying $response before passing it to $next(). This is true.

Offered solution is not to allow passing $response at all. This would fix the problem. However exactly the same effect would be achieved if you modify only $response after calling next $next(). This is something which should be documented in current proposal.

"Why pass $response then at all?" you might ask. There are actual use cases when you want to modify $response before calling $next(). There are actual use cases when you do not call $next() at all but still need the response.

Big part the blogpost describes workarounds how to access the response when it is not passed to the middleware. Workarounds which would not be needed if the $response was passed in the first place.

@michaelcullum
Member

Please keep discussions centralized to one place (on the mailing list for the moment it appears) and only discuss linguistics, spelling, grammar on github, otherwise it's very hard to follow.

@philsturgeon philsturgeon commented on an outdated diff May 30, 2016
proposed/middleware-meta.md
+* Provides a formal standard for middleware developers to commit to.
+* Eliminates duplication of similar interfaces defined by various frameworks.
+* Avoids minor discrepancies in method signatures.
+* Enables any middleware component to run in any compatible framework.
+
+Although many frameworks already use a similar signature, a formal PSR will
+help middleware and framework vendors to formally certify compliance.
+
+[stackphp]: http://stackphp.com/
+
+3. Scope
+--------
+
+## 3.1 Goals
+
+* Provide a middleware interface that is compatible with PSR-7 as a Composer package.
@philsturgeon
philsturgeon May 30, 2016 Contributor

Maybe mention "PSR-7 Http Messages" to keep it clear exactly whats happening. Lots of numbers can confuse.

@philsturgeon philsturgeon commented on an outdated diff May 30, 2016
proposed/middleware-meta.md
+The purpose of this PSR is to provide an interface that defines the formal
+method signature of a PSR-7 compliant HTTP middleware component based on
+existing informal standards.
+
+2. Why Bother?
+--------------
+
+The general concept of a reusable request/response middleware was present in
+[StackPHP][stackphp]. Since the release of PSR-7, a number of frameworks have
+applied the StackPHP concept with a different interface signature. The signature
+for these middleware implementations has been almost universally the same.
+
+Agreeing on a formal middleware interface eliminates several problems and
+provides a number of benefits:
+
+* Provides a formal standard for middleware developers to commit to.
@philsturgeon
philsturgeon May 30, 2016 Contributor

These goals make sense to you and I know what you're getting at, but the trouble is they're likely not clear to the masses.

It almost looks like you're defining a definition with the definition.

"Since PSR-7 has standardized an interface for HTTP requests and responses, we have started to see a huge number of varying implementations of middlewares utilizing these interfaces. Whilst the message interfaces are a great step towards middleware, the actual middleware handlers need to be standardized so that these handlers can be shared between multiple implementations."

  • Provide a formal standard for the structure of middleware "handlers".
  • Eliminates duplication of multiple incredibly similar interfaces defined by various frameworks.
  • Allow sharing of middlewares without minor discrepancies in method signatures breaking things.
  • Enables any middleware component to run in any compatible framework.

Or something.

It's super important to keep language accessible and clear, so we don't have people - who would otherwise be onboard - arguing against it. This happens a lot :D

@philsturgeon philsturgeon commented on an outdated diff May 30, 2016
proposed/middleware-meta.md
+
+* Woody Gilk, <woody.gilk@gmail.com>
+
+### 5.2 Sponsors
+
+* Paul M Jones, <pmjones88@gmail.com> (Coordinator)
+* Jason Coward, <jason@opengeek.com> (Sponsor)
+
+### 5.3 Contributors
+
+* Rasmus Schultz, <rasmus@mindplay.dk>
+
+6. Votes
+--------
+
+* **Entrance Vote:** _(not yet taken)_
shadowhand added some commits May 10, 2016
@shadowhand shadowhand Add proposal for middleware interface
A PSR-7 compatible middleware interface has been defined as an informal
standard already. This takes these existing implementations and creates
a formal standard recommendation.
7611500
@shadowhand shadowhand Add link to entrance vote 41fb839
@shadowhand shadowhand added a commit to shadowhand/fig-standards that referenced this pull request May 31, 2016
@shadowhand shadowhand Add second sponsor
Made Roman coordinator, since Paul is already coordinating another PSR.

Refs #755
e8da4ea
@geggleto

As a follow up to @tuupola's comment, The already existing frameworks have chosen the double pass approach for a good reason. Perhaps if we could get some of them to comment on why they choose the double pass approach in the first place.

@shadowhand
Contributor

I think @pmjones knows the history.

@shadowhand
Contributor
shadowhand commented Jun 6, 2016 edited

@pmjones @opengeek what do I need to do to get this merged? The acceptance vote is passed.

@pmjones
Contributor
pmjones commented Jun 6, 2016

ping @michaelcullum @svpernova09 @squinones

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

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

@reinink
Contributor
reinink commented Jun 6, 2016

@svpernova09 Will do!

@shadowhand shadowhand added a commit to shadowhand/fig-standards that referenced this pull request Jun 13, 2016
@shadowhand shadowhand Add second sponsor
Made Roman coordinator, since Paul is already coordinating another PSR.

Refs #755
69580f8
@asika32764 asika32764 referenced this pull request in ventoviro/phoenix Jun 14, 2016
Closed

-t=simple δΈζœƒη”’η”Ÿ migration file #157

@shadowhand shadowhand added a commit to shadowhand/fig-standards that referenced this pull request Jun 15, 2016
@shadowhand shadowhand Add second sponsor
Made Roman coordinator, since Paul is already coordinating another PSR.

Refs #755
bb49b6b
@shadowhand shadowhand deleted the shadowhand:middleware branch Jun 15, 2016
@mindplay-dk
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
Contributor

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

@mnapoli
Member
mnapoli commented Aug 15, 2016

πŸ‘ and https://groups.google.com/forum/#!topic/php-fig/V12AAcT_SxE regarding FrameInterface

@mindplay-dk
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
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
Contributor
mindplay-dk commented Aug 16, 2016 edited

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
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
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
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
Contributor
mindplay-dk commented Aug 16, 2016 edited

@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
Contributor
mindplay-dk commented Aug 16, 2016 edited

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
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
Contributor

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
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
Contributor

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
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
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
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
Member
mnapoli commented Aug 17, 2016

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

@weierophinney
Contributor

@mnapoli Yes, the current FrameInterface.

@mnapoli mnapoli added a commit that referenced this pull request Aug 17, 2016
@mnapoli mnapoli Explain the signature of FrameInterface
This documentation is a transcript of the discussions in https://groups.google.com/d/topic/php-fig/V12AAcT_SxE/discussion and #755
a1dfffb
@mnapoli
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
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