http-interop / http-middleware

PSR-15 interfaces for HTTP Middleware
MIT License
73 stars 7 forks source link

Callable delegates #37

Closed mindplay-dk closed 7 years ago

mindplay-dk commented 7 years ago

I hate to bring this up again, but...

A lot of people have expressed a dislike for the single-method DelegateInterface and want to be able to to just return $next($request) in their middleware implementations. That's not a big deal for me, but several people seemed annoyed, and wanted to put __invoke() in the interface because of that, etc.

In mindplay/middleman, I have this silly implementation of DelegateInterface that simply wraps a callable, which is also completely redundant and wasteful - creating a meaningless extra object wrapper for every middleware that gets dispatched, for perceived type-safety (and, okay, yes, stricter static analysis) but it's really just moving the point of failure one layer away from the middleware itself if it passes a non-request to the delegate.

Well, that's two issues, but then I just ran into another one.

I have a case where I'd like to call this middleware, which does client IP detection, but from outside the context of a middleware stack.

In other words, I want to just run a single middleware component.

With a callable delegate, that was really simple - I might just do:

$client_ip = new ClientIp();

$ip = $client_ip->process($request, function ($r) { return $r; })->getAttribute("client-ip");

But I can't do that.

I need an actual implementation of DelegateInterface and I have to either implement and create an instance of that, or import and instanciate an entire middleware stack - for no real reason, other than to satisfy the type-hint.

This is not an odd case - I've had plenty of cases in the past, where I compose middleware out of several other internal middleware components, for example, conditionally dispatching different middleware instances, etc.

In the light of all these issue, I have to bring up this subject again - sorry.

What I keep coming back to, is that callable for delegates, in de-facto standard PSR-7 middleware, worked fine - it wasn't causing anyone any real headaches, and I think the problem that stricter type-hinting solves in this case is really mostly theoretical.

The DelegateInterface, on the other hand, seems to have rubbed several people the wrong way, and seems to breed other problems and complexities that wouldn't exist with a plain callable.

Who are we supposedly helping with this extra abstraction around delegates?

Maybe the reason this interface (and it's method) was so hard to name, is because it's really honestly serving no real purpose beyond stricter type-hinting - in a place that wasn't really being reported as a problematic pain point by anyone in the first place.

Are we absolutely freakin' sure that this is the right approach?

shadowhand commented 7 years ago

The example you gave is kinda funky, because you're using the delegate to return the request, instead of producing a response. I understand why you are doing it here, it's just not following the standard.

However, I have always been of the opinion that preventing people from doing clever things (so long as they are safe) is more important than forcing a particular pattern of usage. As I have mentioned previously I prefer a callable delegate.

That said, what you are proposing goes a step beyond that and implies that we would go back to callable $next, without a class type hint. I'm really on the fence about that.

@weierophinney can you chime in here? Would having a callable $next be better or worse for Zend's dispatching systems?

schnittstabil commented 7 years ago

I fully agree. IMO, standards are about contracts, not about interfaces. E.g. in JavaScript we don't have a Class/Type/Object for Iterables – ES2015 just describe how iterable instances must behave – no class need, e.g. like the Promise class they have. They just say, that they standardize the protocol, i.e. they describe the methods which an iterable must have and how those methods must behave!

Thus, I suggest to do same:

interface DelegateInterface
{
    /**
     * Dispatch the next available middleware and return the response.
     *
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     */
    public function __invoke(RequestInterface $request);
}

interface ServerMiddlewareInterface
{
    /**
     * Process an incoming server request and return a response, optionally delegating
     * to the next middleware component to create the response.
     *
     * @param ServerRequestInterface     $request
     * @param callable|DelegateInterface $delegate
     *
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, callable $delegate);
}

And then, we just tell everybody: Okay, you will get a callabe which is maybe a DelegateInterface, but for sure, the callable must fulfill the contract of DelegateInterface: i.e. it's first argument must typehint against RequestInterface and it must return a ResponseInterface or throw an Exception.

We cannot force process():ResponseInterface anyway in PHP<7.

Furthermore, and that is crucial for my #35 issue, currently the caller must provide a delegate which fulfills the comment as well:

Dispatch the next available middleware and return the response.

Wich is not possible in your example.


Furthermore, AFAIK, it is possible to type check __invoke(RequestInterface $request) and function(RequestInterface $request){} during runtime by reflection in PHP 5.6. This is not ideal, but I hope changing to __invoke (for the DelegateInterface!) is not a BC issue…

shadowhand commented 7 years ago

and it must return a RequestInterface or throw an Exception

I assume you meant ResponseInterface here. And there's nothing in the spec about throwing an exception.

schnittstabil commented 7 years ago

And there's nothing in the spec about throwing an exception.

That is true, but if we want to describe the behavior in depth, we should say that it is possible: e.g. creating an Response instance may throw an Exception or Throwable or …. Thus we need to allow either throwing or return null. I think exceptions fit much much better…

That would not be necessary, if we stick with the interface – just because interfaces allows exception throwing…

shadowhand commented 7 years ago

In what situation would it be okay to fail creating a response? That would imply a serious misconfiguration on the user's part and allowing it would imply that every middleware would need to try/catch the delegate call.

schnittstabil commented 7 years ago

In what situation would it be okay to fail creating a response?

That depends on the concrete implementation of the Response object. But it is also possible, that we may run out of memory or similar. Thus an implementer must do in PHP 7:

class D implements DelegateInterface
{
    public function __invoke(RequestInterface $request)
    {
        try {
            return new Response();
        } catch (Throwable $error) {
            return $someMock();
        }
    }
}

That would imply that every middleware would need to try/catch the delegate call.

I disagree, we should only state that it is possible that a delegate/requesthandler MAY throw an Exception/Error/…, not that the caller MUST handle it…


EDIT: On the other hand, it would be totally sufficient, to state that the callable must implement the interface behavior, or a similar description. Thus we don't need to address where and how exceptions are handled in PHP – it is obvious to most readers…

mindplay-dk commented 7 years ago

@schnittstabil what you demonstrated in the code sample above is more less precisely what I was doing in mindplay/middleman in version 1. See here.

schnittstabil commented 7 years ago

Maybe, I should self-throttle, but I think it is worth to note.

If we switch to:

interface DelegateInterface
{
    /**
     * Process a server request and return the response.
     *
     * @param ServerRequestInterface $request
     *
     * @return ResponseInterface
     */
    public function __invoke(ServerRequestInterface $request);
}

And we tweak the ServerMiddlewareInterface comment (drop the next middleware thing), then almost all my concerns I've mentioned at #35 will vanish:

class AwesomeMiddleware implements ServerMiddlewareInterface
{
    /**
     * Process an incoming server request and return a response, optionally delegating
     * the request utilizing $delegate.
     *
     * @param ServerRequestInterface     $request
     * @param DelegateInterface|callable $delegate
     *
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, callable $delegate) {
        $response = $delegate($request);
        return $response->withHeader('X-PoweredBy', 'Unicorns');
    }
}

To me the wording would be a bit odd, but:

schnittstabil commented 7 years ago

@mindplay-dk Nice, similar to what I came up. You may want to take a look at my brand new Cormy Onion middleware stack: it supports callables too (e.g. by its constructor) – However, it utilizes PHP7 Generators, thus it is not really PSR-15 related…

mindplay-dk commented 7 years ago

I think what @schnittstabil is proposing is spot-on.

I really don't care if calls to delegates are checked - the delegate itself is provided by the middleware stack, so the type-check can just happen there, the net effect is the same.

This is much simpler, we get static inspections either way, and we can have run-time type-checks either way - just requires a type-check in dispatchers.

It's not like we're watertight on type-checking anyway, with PHP 7 return type-hints missing.

schnittstabil commented 7 years ago

I like the idea more and more.

Let's say we do the same with ServerMiddlewareInterface:

A middleware is a callable, which fulfills the ServerMiddlewareInterface contract.

@weierophinney may throw his hands up in horror :grin: – but, if I'm not wrong, it allows soft migration for Stratigility:

class Middleware implements \Zend\Stratigility\MiddlewareInterface
{
    // domain specific
    private function oracle(ServerRequestInterface $request)
    {
        return (new Response())->withHeader('X-Answer', '42');
    }

    // Stratigility version
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $out = null)
    {
        return $this->oracle($request);
    }

    // PSR version 
    // DelegateInterface or callable depends on the Stratigility pipe interface…
    public function foo(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        return $this->oracle($request);
    }
}
$middleware = new Middleware();

// Stratigility
$app->pipe('/', $middleware);

// PSR
$app->pipe('/', [$middleware, 'foo']);
shadowhand commented 7 years ago

@schnittstabil I don't think your middleware example works through all the details. The delegate is being dropped from both handlers. Let's stick to discussing the delegate here, not middleware signature.

atanvarno69 commented 7 years ago

With a callable delegate, that was really simple - I might just do:

$client_ip = new ClientIp();

$ip = $client_ip->process($request, function ($r) { return $r; })->getAttribute("client-ip");

But I can't do that.

I don't see how this is particularly different from what you want to do:

$client_ip = new ClientIp();

$ip = $client_ip->process($request, new class ($r) implements DelegateInterface {
    public function __construct($r) {
        $this->r = $r;
    }
    public function process(RequestInterface $request) {
        return $this->r;
    }
})->getAttribute("client-ip");

If the delegate is type hinted callable rather than DelegateInterface there is no longer a contractual guarantee that middleware can call $delegate->process($request) and be sure of receiving a response. DelegateInterface::process($request) returns a ResponseInterface, $callable($request) returns whatever it likes. Instead, middleware has to assume responsibility for checking the delegate's return value if I am being defensive:

public function process(ServerRequestInterface $request, callable $delegate)
{
    $response = $delegate($request);
    if (!$response instanceof ResponseInterface) {
        throw new UnexpectedValueException(); // or some recovery action
    }
    return $response;
}

In the worst case the delegate could return an object that does not in fact implement ResponseInterface, but provides some identically named method. So without the type checking above, the middleware could act on the 'response' using that named method, then return the result. My middleware, through no fault of its own, has now failed to implement PSR-11 as it can return a non-ResponseInterface instance. I can't use PHP 7 return types to notify the user that my return type is not as expected, because the PSR has to support PHP 5.6, so I have to check the delegate's return value.

Surely, the point of the specification is to allow me to write middleware without any knowledge of the delegate, other than that provided by DelegateInterface. The docblock in the interface gives me a gentleman's agreement, not a concrete guarantee enforced by type hinting.

schnittstabil commented 7 years ago

If the delegate is type hinted callable rather than DelegateInterface there is no longer a contractual guarantee that middleware can call $delegate->process($request) and be sure of receiving a response. DelegateInterface::process($request) returns a ResponseInterface, $callable($request) returns whatever it likes.

At first, I think you meant $delegate($request) (w/o ->process). Secondly, the @return ResponseInterface comment means nothing to the PHP type system. Thus, in your example above, it is absolutely valid if you do:

    /**
     * @return ResponseInterface
     */
    public function process(RequestInterface $request) {
        return 42;
    }

It's likely that your IDE/linter would complain about that, but not PHP itself.

The contractual guarantee is provided by the PSR-15 documentation itself, i.e. by the comments of the interfaces, the method signatures etc. Thus, for you, as a delegate implementer, the PSR guarantees that a your callable will only be called with RequestInterface instances, if your delegate is used by a middleware or a middleware container.

Hence, it is not a gentleman's agreement: middleware containers (stacks/pipes etc.) can only claim to be PSR-15 compatible, if they fulfill the PSR-15 contract. That's not new, you already rely on that, e.g. if you call $delegate->process in $m0 a compatible middleware pipe MUST call the very next middleware $m1:

$pipe = new MiddlewarePipe([
    $m0,
    $m1,
    $m2,
]);

– Of course, same holds for DelegateInterface implementers as well…

schnittstabil commented 7 years ago

@atanvarno69 I just read your examples again and maybe the following is obvious to you, but not to every read-only participant.

DelegateInterface won't go away[*], you can still use them:

$ip = $client_ip->process($request, new class ($r) implements DelegateInterface {
    //…
    public function __invoke(RequestInterface $request) {
        //…
    }
})->getAttribute("client-ip");

That not only alleviate at least some IDE/linter issues, but will also help middleware containers which need to type-check delegates at run-time because of backward compatibility concerns.

[*] But be aware of #38

atanvarno69 commented 7 years ago

@schnittstabil, thank you for your replies. I accept your point about relying on an implementation to fulfill the terms of the PSR-15 contract.

However, in just over two years PHP 5.6 will no longer be supported. At which point, this PSR could (should?) be updated (to version 2.0.0) or superseded (by a PSR with a new number - I am not sure which way FIG would do this) to use return types. The interfaces could become something like:

interface ServerMiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate): ResponseInterface;
}

interface DelegateInterface
{
    public function process(RequestInterface $request): ResponseInterface;
}

Which would solve my current jitters about type safety. Whereas a return type on DelegateInterface::__invoke() does not, as it would still be possible to pass any callable to ServerMiddlewareInterface::process(). I'm sure this a fairly minor point, but I think it should be considered that, while we are not there now, the PHP 7+ world will allow better type safety. So these interfaces should be built with that future upgrade in mind.

That aside, if ServerMiddlewareInterface does switch from type hinting DelegateInterface explicitly to callable, it would not be the end of the world. I am just failing to see the concrete benefits it gives over typehinting an interface.

Indeed, if no code is ever going to hint against DelegateInterface, why does it even exist? Should the specification instead just define an acceptable callable signature?

A further thought: I can imagine that some point in the future there may be a Middleware Stack/Pipe PSR which will want to define something like:

ExtendedDelegateInterface extends DelegateInterface
{
    public function push(ServerMiddlewareInterface $middleware);
    // Other methods for interacting with the stack or pipe
}

Type hinting callable seems to preclude using that sort of method.

Note that example is purely theoretical, and intended only to illustrate that process() allows room for possible extension in future while __invoke() does not.

Again, this is not a deal breaker, I just want to make sure it is considered.

Returning to my not seeing the benefits of type hinting callable over DelegateInterface. As far as I can see there were three given by the first post:

  1. "A lot of people have expressed a dislike for the single-method DelegateInterface and want to be able to to just return $next($request) in their middleware implementations. [...] several people seemed annoyed, and wanted to put __invoke() in the interface."

    Without knowing why that is annoying, I am not sure if I should care. I certainly don't care if it is merely a statement of preference, rather than a concrete reason. I will make an unfounded assumption: people are annoyed because they write invoke-middleware now and don't like change. If so, I draw analogy to PSR-7: it was annoying to switch from Symfony\HttpFoundation to PSR-7, but the benefits of adoption and interoperability have proven themselves and improved the PHP ecosystem.

  2. "In mindplay/middleman, I have this silly implementation of DelegateInterface that simply wraps a callable, which is also completely redundant and wasteful - creating a meaningless extra object wrapper for every middleware that gets dispatched, for perceived type-safety (and, okay, yes, stricter static analysis) but it's really just moving the point of failure one layer away from the middleware itself if it passes a non-request to the delegate."

    Partly, this is an issue arising from refactoring from one architecture to another, which will always be painful and cumbersome to implement with a wrapper. See above on disliking change. Also, when new middleware systems are written (and they will be when this PSR is accepted), they will be written to these interfaces and not concern themselves with support pre-PSR-15 middleware. Of course, existing good packages will want to maintain backwards compatibility, but as proven by mindplay/middleman that is simple, if a bit ugly.

    Yes, the type-safety is perceived, currently. But hopefully when PHP 5.6 is a thing of the past that type-safety can become actual.

    Finally, stricter static analysis ain't nothing.

  3. The third point was about providing a response for a single middleware to use it outside of a stack/pipe. My previous post gave an example using an anonymous object rather than an anonymous function, which to my mind is effectively the same solution to the problem in this case. Yes it is a little bit longer to type, but that is the cost of using middleware outside of the environment they were designed for. (I am not saying that sort of thing should be prohibited. Clever, useful tricks that get jobs done should be permitted. And I think this one is. But as @shadowhand pointed out, it is not following the standard.)

So... I'm not seeing the benefit, when stacked against better static analysis, (hopefully) future type-safety and (potential) future extensibility.

DelegateInterface's method being __invoke() is not preferred, but I would live with it. I just can't see why ServerMiddlewareInterface::process() should type hint against callable, though.

I leave the decision to smarter and more experienced minds than mine to sort out, I just hope my comments are considered (even if they are judged ill-informed and dismissed).

schnittstabil commented 7 years ago

To be honest, I would love to target only PHP7, but I feel neither responsible nor sufficiently informed enough to start a debate about that.

However, if we target PHP7+, then most of your concerns may vanish:

Thus, it is not an __invoke vs callable problem, it is about PHP 5 vs 7.

Btw, you can already write:

new class implements DelegateInterface {
    public function process(RequestInterface $request):ResponseInterface {
        return …;
    }
}

Of course, you will lose support for PHP < 7…


I believe many PHP developers are a bit functophobic, but writing function packages for composer has some drawbacks. Thus, as a package maintainer, I would most likely want to write classes which implement the interface versions.

On the other hand, many consumers have to write domain specific middlewares/delegates as well. Most notably actions/controllers or similar, and they don't want to write classes for every single middleware/delegate. There are many examples, where microframeworks like Slim, Silex, Laravel and many other have their advantages:

$app = new \Slim\App;

$app->get('/contact/{name}', function (Request $request, Response $response) {
    $name = $request->getAttribute('name');
    $response->getBody()->write("Hello, $name");
    // …

    return $response;
});

$app->post('/contact', function (Request $request, Response $response) {
    $name = $request->getAttribute('name');
    // … send an email …
    $response->getBody()->write('Thank you for you comment');

    return $response;
});

$app->run();

For them, it would be cumbersome to write new class implements DelegateInterface/MiddlewareInterface {…}, it will clutter their files and it will distract them from getting a simple job done.

schnittstabil commented 7 years ago

Sorry, I've forgotten to address your ExtendedDelegateInterface:

ExtendedDelegateInterface extends DelegateInterface
{
    public function push(ServerMiddlewareInterface $middleware);
}

Thanks for bringing up this idea. But it would be still possible, and has nothing to do with __invoke vs process. It is just an inheritance problem. Let's assume a stack would implement it, and want to call a middleware which is aware of that interface:

class Middleware extends ServerMiddlewareInterface
{
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        if ($delegate instanceof ExtendedDelegateInterface) {…}
    }
}

No problem. However, what neither now nor later will work:

class Middleware extends ServerMiddlewareInterface
{
    public function process(ServerRequestInterface $request, ExtendedDelegateInterface $delegate)
    {
    }
}
atanvarno69 commented 7 years ago

To be honest, I would love to target only PHP7, but I feel neither responsible nor sufficiently informed enough to start a debate about that. However, if we target PHP7+, then most of your concerns may vanish:

  • the interface signature may look like __invoke(RequestInterface $request):ResponseInterface [...]

Thus, it is not an __invoke vs callable problem, it is about PHP 5 vs 7.

Agree. Sadly, the PSR must target all supported PHP versions, so for now we're stuck with 5.6+.

My point about possibly (hopefully) adding return types in future:

Which would solve my current jitters about type safety. Whereas a return type on DelegateInterface::__invoke() does not, as it would still be possible to pass any callable to ServerMiddlewareInterface::process().

was not arguing against the DelegateInterface method being __invoke(). It was arguing against the type hint in ServerMiddlewareInterface being callable, which I don't think was clear.

  • the is_delegate function will check the return type as well

I'm not sure what you mean by this? I'm not aware of an is_delegate function, can you clarify, please?

There are many examples, where microframeworks like Slim, Silex, Laravel and many other have their advantages

[examples]

Yes. But the PSR is targeted at frameworks. While I would not expect immediate adoption, if they did choose to adopt the PSR in their next major versions, I would expect Slim, Silex, Laravel, et al, to provide ways and means for their users to easily write delegates (perhaps akin to mindplay/middleman by having a wrapper that the route definition methods wrap their controller delegates in which would be invisible to the user, or some other clever system). This, to me, is an implementation detail for those microframeworks, if they choose to adopt the standard. The fact that the proposed standard in this use case can not be adopted overnight does not make the standard worse. PSR-7 required getting used to immutability when the previous defacto standard did not and it meant getting used to it. Once people got it, they saw its benefits and reaped the rewards of greater interoperability.

But there are still frameworks not using PSR-7, and that is fine. They work and allow their users to solve the problems they need to solve. Likewise if PSR-15 does not make sense for a given developer's use case, they do not need to use it. Framework providers who do not think interoperability is right for their domain will not implement and nor should they. Those who do want interoperability will, with their next major version, make it as painless as possible for their users.

Re: the reply on the hypothetical future ExtendedDelegateInterface, my point was that type hinting callable in the ServerMiddlewareInterface method precludes future extensions to DelegateInterface, not that __invoke() should not be used as the method name for DelegateInterface. The line in my last reply:

Note that example is purely theoretical, and intended only to illustrate that process() allows room for possible extension in future while __invoke() does not.

was wrong, and should have been talking about type hints. My apologies, I was typing my reply quickly and must have confused myself.

A brief summary of my opinion:

I think I should self-throttle for a while and let others chime in.

schnittstabil commented 7 years ago

@atanvarno69 Sorry, I'm a bit short of time – I'll thoroughly answer your question tomorrow. In the meantime you may want to read #39 and its is_delegate function – these may answer some questions…

schnittstabil commented 7 years ago

Yes. But the PSR is targeted at frameworks.

Yes, but it should not be targeted at frameworks only! For established frameworks, I fully agree its easy for them to create some adapters or similar, but every last framework, stack and pipe must do that, if they want to provide a dispatching mechanism!

I want to see innovative frameworks in the future with nifty interfaces which I cannot imagine in my wildest dreams. Why should we impede them, if we are not forced to?

I also foresee many, many packages at packagist, which are fully dedicated to PSR-15. Today, I've written, for the very first time a stack implementation based on #39 in 12 MINUTES!

Okay, to be fair, I've written stacks for PSR-7 before and I knew why #39 and #35 are so awesome. But it was so freakin easy to write those 25 LOC in a single class! After I reused my tests from my generator-based middleware project Cormy Onion, the whole project was set up in almost 2 hours 1 1/2 hours!

Take a look at the two most simple middleware containers I'm aware of (see Awesome PSR-15 Middlewares for more), which are more or less comparable to mine:

(All LOC were calculated by https://insight.sensiolabs.com)

The whole Stack/implementation w/o comments:

class Stack implements DelegateInterface
{
    protected $core;
    protected $middlewares;

    public function __construct(callable $core, callable ...$middlewares)
    {
        $this->core = $core;
        $this->middlewares = $middlewares;
    }

    public function __invoke(RequestInterface $request):ResponseInterface
    {
        if (count($this->middlewares) === 0) {
            $core = $this->core;

            return $core($request);
        }

        $copy = clone $this;
        $topMiddleware = array_pop($copy->middlewares);

        // this could be $topMiddleware(…) as well:
        return $topMiddleware->process($request, $copy);
    }
}

tl;tr Writing this comment took me approximately 6 times longer than writing the whole Stack, thus simplicity and reusability are the benefits I see.

schnittstabil commented 7 years ago

Re: the reply on the hypothetical future ExtendedDelegateInterface, my point was that type hinting callable in the ServerMiddlewareInterface method precludes future extensions to DelegateInterface

PSRs are superseded. But if you thought about the following extension:

 interface ServerMiddlewareInterface
 {
-     public function process(ServerRequestInterface $request, DelegateInterface $delegate);
+     public function process(ServerRequestInterface $request, ExtendedDelegateInterface $delegate);
 }

Then, I must say that this is not how inheritance work and it would break backward compatibility: It wouldn't be possible anymore to call new middlewares with non-extended DelegateInterfaces.

On the other hand, with callables you may get exactly what you want:

class Middleware extends ServerMiddlewareInterface
{
    public function process(ServerRequestInterface $request, callable $delegate)
    {
        if ($delegate instanceof ExtendedDelegateInterface) {…}
    }
}
schnittstabil commented 7 years ago

A callable-style Pipe w/o comments:

class Pipe implements ServerMiddlewareInterface
{
    protected $middlewares;

    public function __construct(ServerMiddlewareInterface ...$middlewares)
    {
        $this->middlewares = $middlewares;
    }

    public function process(ServerRequestInterface $request, callable $finalHandler)
    {
        return $this->processMiddleware(0, $request, $finalHandler);
    }

    private function processMiddleware(int $index, ServerRequestInterface $request, callable $finalHandler)
    {
        if (!array_key_exists($index, $this->middlewares)) {
            return $finalHandler($request);
        }

        $current = $this->middlewares[$index];

        return $current->process(
            $request,
            function (ServerRequestInterface $req) use ($index, $finalHandler) {
                return $this->processMiddleware($index + 1, $req, $finalHandler);
            }
        );
    }
}

31 LOC, one single classcallable FTW!

(All LOC were calculated by https://insight.sensiolabs.com)

shadowhand commented 7 years ago

@schnittstabil I feel like this last example went a bit off the rails... why does the middleware need to be callable? I thought we were only talking about callable delegates...

schnittstabil commented 7 years ago

@shadowhand updated

mindplay-dk commented 7 years ago

why does the middleware need to be callable? I thought we were only talking about callable delegates...

I agree.

If somebody wants to support callable|ServerMiddlewareInterface (or even other types) in their middleware-stack, they will have to do type-detection either way - so there is no reason for us to occupy or rely on the magic __invoke() method.

schnittstabil commented 7 years ago

Thanks to the discussion at https://github.com/http-interop/http-middleware/pull/39#issuecomment-263561695, I dont think it is worth to type-hint against callable for delegates within the ServerMiddlewareInterface anymore (I still believe they should be __invokeable, thus Stack/Pipe implementers can type-hint against callable w/o type-detection and they could be reused etc.).

@mindplay-dk Why not create a utility adapter package for such cases, they are not the standard way, because you return a request instead of a response – and obviously they shouldn't be considered as standard.

namespace mindplay\middleman;

class DelegateAdapter implements DelegateInterface
{
   protected $adaptee;

    public function __construct(callable $adaptee) {
        $this->adaptee = $adaptee;
    }

    public function process(RequestInterface $request) {
        $adaptee = $this->adaptee;

        return $adaptee($request);
    }
}
$ip = $client_ip->process($request, new DelegateAdapter(function ($r) {
   return $r;
}))->getAttribute("client-ip");

Furthermore, you can additionally create a utility function:

namespace mindplay\middleman;

function to_delegate(callable $d) {
   return new DelegateAdapter($d);
}
use function mindplay\middleman\to_delegate;

$moreStuff = …;

$ip = $client_ip->process($request, to_delegate(function ($r) use ($moreStuff){
   return $r;
}))->getAttribute("client-ip");

Nevertheless, you must provide a delegate which fulfills the phpdoc as well:

Dispatch the next available middleware and return the response.

With my suggestion you break the contract in two ways:

  1. you do not return a response
  2. you do not call/dispatch the next middleware

Thus, – and that is independent from point 1. – you do not know if your middleware would behave as expected in your context, just because it is not used within a middleware container.

mindplay-dk commented 7 years ago

That is what I do.

It's not difficult - certainly not worthy of a separate package, in my opinion.

As argued earlier though, such a class provides no actual insulation against type-errors, you still have to check at run-time, but - above all - this was never a problem for anyone with the de-facto standard, no one ever complained about this.

In my opinion, we've added complexity in order to provide training wheels to something that never caused anyone any difficulty in the first place.

Frankly, I'm tired of debating it.

Nothing new has brought to light, and I've said what I have to say on this subject.

You guys keep debating it, if you feel it's worth while - I don't have the capacity to keep arguing the same points over and over again. Sorry guys...

schnittstabil commented 7 years ago

The reason, I've suggested to create a new package might not be important to you in person, but mindplay\middleman\Delegate is not reusable:

  1. you have marked it @internal
  2. you cannot use it within another middlewares or other middleware containers w/o installing your mindplay/middleman.

Beside that this was the whole new point, I just wanted to improve your current situation as well as for others…

mindplay-dk commented 7 years ago

but mindplay\middleman\Delegate is not reusable

It's not supposed to be reusable - it's an implementation detail, and therefore marked as @internal.

In my view, you're proposing to add more complexity for something that is already unnecessarily complex and completely redundant - a separate package to manage (tests, versioning, conflict resolution, etc.) for something that is already a trivial implementation detail, that just makes things worse.

It's marked @internal because it's an implementation detail that really doesn't affect anyone or anything - it's frustrating to need to have it in the first place, because it has zero purpose or justification, beyond providing a run-time type-check, which should be performed at statically at design-time anyway, and which could just as easily be performed with a line of code in the middleware-stack, or even a type-hint on the callable generated by it, and everything would work just the same.

If you can understand that and still insist that it's necessary, then we simply do not agree.

Beside that this was the whole new point, I just wanted to improve your current situation as well as for others…

I appreciate that, thank you, but I don't agree that this improves the situation for me and (especially not) for anybody else.

It's OK for us to disagree - I'm not trying to be rude about it, I just honestly don't have the energy to keep debating it.

mindplay-dk commented 7 years ago

The example you gave is kinda funky, because you're using the delegate to return the request, instead of producing a response. I understand why you are doing it here, it's just not following the standard.

@shadowhand yeah, you're right, looks like I messed up - what I should be doing was likely something like:

$ip = $client_ip->process($request, function ($req) {
    return (new Response())->withAttribute("client-ip", $req->getAttribute("client-ip"));
})->getAttribute("client-ip");

So yeah, that's more correct, but not exactly pretty.

Likely the correct solution in this particular case is actually to factor out the client IP detection function into a separate component, or a public static function, making it available outside of a middleware context.

My approach was a wonky work-around.

schnittstabil commented 7 years ago

:tada: Congrats to @mindplay-dk: you have encounter an anti-pattern of middlewares :tada:

Exactly the same idea came today to my mind:

https://github.com/middlewares/client-ip/blob/master/src/ClientIp.php#L85-L90:

-    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
+    public function process(ServerRequestInterface $request)
    {
        $ip = $this->getIp($request);
-        return $delegate->process($request->withAttribute($this->attribute, $ip));
+        return $request->withAttribute($this->attribute, $ip);
    }

Calling delegate is Continuation-passing style without any benefit. Depending on your environment, it clutters your stack traces and hit your performance.

After refactoring those middlewares into requestParsers, you can use it on the client and server side – which reduces the need for supporting client middlewares as well. Of course, we can use those requestParsers within middlewares – but I suggest to always consider the following usage first, essentially:

$pipe = [
   function ($request, $delegate) {
      $request = addIp($request);
      $request = addUnicorns($request);
      $request = addFoo($request);

      return $delegate->process($request);
   },
   // …
];
mindplay-dk commented 7 years ago

@schnittstabil well, I didn't write that middleware, I was trying to leverage it's functionality outside of a middleware-stack context. What I should have done is refactor and send a PR :-)

mindplay-dk commented 7 years ago

@schnittstabil @shadowhand we need to conclude no this issue.

Can we try callable|DelegateInterface as the type-hint and simply see how that pans out in review?

It's what we have today with the de-facto standard, so I doubt it will come as a shock to most of the community.

The only new argument I've heard against it, is this:

In the worst case the delegate could return an object that does not in fact implement ResponseInterface, but provides some identically named method. So without the type checking above, the middleware could act on the 'response' using that named method, then return the result. My middleware, through no fault of its own, has now failed to implement PSR-11

However, this is easily addressed by type-checking both input and output in middleware-stacks. The result is same-same; with proper doc-blocks, you still get proper static analysis.

I can PR the documentation and interface updates if you'd like?

schnittstabil commented 7 years ago

with proper doc-blocks, you still get proper static analysis

I really like this argument, but I do not think it is applicable in our context:

interface ServerMiddlewareInterface
{
    /**
     * @param callable|DelegateInterface $delegate
     */
    public function process(ServerRequestInterface $request, callable $delegate);
}

Your QA tools cannot guess that a $delegate will always return a ResponseInterface instance.

If we want callable then I believe there is nothing left but to guarantee this invariant by the phpdoc and the standard itself…

EDIT: Mhm, we may also add to the standard:

A middleware container MAY/SHOULD call middlewares only with a DelegateInterface instance

But I believe neither MAY nor SHOULD would be useful.

atanvarno69 commented 7 years ago

If the interface is

interface ServerMiddlewareInterface
{
    /**
     * @param callable|DelegateInterface $delegate
     */
    public function process(ServerRequestInterface $request, callable $delegate);
}

Then nowhere (in code, not docblock) are we type hinting against DelegateInterface, which then raises the question: why do we have DelegateInterface at all, since it is not used by the code? If this is to be the interface (which I hope it will not be), DelegateInterface should be removed from the spec and replaced with a specification (in words, not an interface) for a compliant delegate.

schnittstabil commented 7 years ago

@atanvarno69 Have you read my https://github.com/http-interop/http-middleware/pull/39#issuecomment-263150726? – I don't think we should remove the interface, even with callable.

atanvarno69 commented 7 years ago

@schnittstabil Yes, and I agree with the counter points provided by @DaGhostman (this, as well as his various other replies).

schnittstabil commented 7 years ago

@atanvarno69 Sorry, I think that is a misunderstanding, all I'm saying is:

If we go with callable, then I don't think we should remove the interface.

I can't see that @DaGhostman argues against that.

schnittstabil commented 7 years ago

And, just to clarify, the docblock may look like:

interface ServerMiddlewareInterface
{
    /**
     * …
     * Enforcement:
     *    Middleware container MUST/MAY/SHOULD call process only with
     *    DelegateInterface instance
     *
     * @param callable|DelegateInterface $delegate
     * …
     */
    public function process(ServerRequestInterface $request, callable $delegate);
}

Obviously, if we use 'MUST' instead of 'MAY' or 'SHOULD', then we can drop those lines and type-hint against DelegateInterface. But I agree, with this approach, the current parser (QA or PHP itself) will ignore that comment.

Maybe you are wondering why I capitalize those words: Key words for use in RFCs to Indicate Requirement Levels.

atanvarno69 commented 7 years ago

@schnittstabil The point I intended to link to:

My point against type-hinting against callable is that: You cannot enforce anything when you use callable, a valid callable syntax is:

  • Class that has __invoke defined
  • closure
  • Array with class name and method name: ['ClassName', 'method']
  • String of class name and static method: ClassName::method

If that is the case and the interface is defined in such a way to allow that will allow bad practices (in my understanding). In some time PHP7 will become mainstream (a lot of companies start adopting it) and we will end up having to a) make a new standard because this one will not be enforceable; or b) it will no be used (defeats the purpose of it); or c) Majority will not actually adopt type hints and we will still have spaghetti written (think PHP4-style code, in PHP5, which became an issue with removing PHP4 constructors in 7 as a lot of projects ware using them and will slow adoption, hence deprecated and will have to drag them till next major version, which is god knows how far ahead).

More succinctly, earlier:

@schnittstabil, with the dual type-hint in the comment or type-hinting against callable, you successfully achieved making the interface pointless. When you accept a callable you can get whatever, nor you can enforce the arguments of said callable, unless you type-hint to the interface, which defeats the purpose of using callable in first place.

You give your reasons for the interface still be worthwhile in the spec. Paraphrasing: The interface still defines the contract and is available for reference in an IDE.

I do not disagree that these are useful, however they are not the purpose of an interface. An interface is appropriate to define a contract, to my mind, if and only if it will be coded against (used as a type hint). If middleware does not type hint against it then nothing does, and so the interface is pointless. The contract should instead be spelled out in the spec (which is its purpose). The IDE reference would be nice, but it is no reason to provide a useless interface.

To my mind the ideal would be to keep the interface and actually use it, i.e. type hint against it. That way the contract is defined in code that is used and is available for IDE reference (and enforcement).

schnittstabil commented 7 years ago

I've mentioned more arguments than the IDE – I'm not really interested in IDEs, to me they are just a nice editors with some QA: they sometime show me that I've a typo in a method name or similar.

The most important one for me:

In summary: I can statically proof that this class will provide the right typing. And I can document that this class fulfills the delegate contract


An interface is appropriate to define a contract, to my mind, if and only if it will be coded against (used as a type hint).

Maybe you find this argument more valid:

For middleware container implementors: I may want to implement to an interface rather than dealing with functions, e.g. Equip\Dispatch\Delegate

Thus, I strongly believe stacks, pipes etc. will code against it and proof their compatibility.

atanvarno69 commented 7 years ago

For middleware container implementors: I may want to implement to an interface rather than dealing with functions, e.g. Equip\Dispatch\Delegate

Thus, I strongly believe stacks, pipes etc. will code against it and proof their compatibility.

Granted. However, if middleware doesn't type hint against DelegateInterface, delegate providers could just as easily not code against DelegateInterface and produce a compliant delegate. DelegateInterface is provided for the convenience of stack/pipe/whatever providers. A specification should only provide what is necessary and sufficient to its implementation. It is sufficient to provide a stack/pipe/whatever that fulfills the specification, and thus DelegateInterface is redundant, unnecessary.

If FIG want to provide for stack/pipe/whatever providers DelegateInterface in a world where middleware does not type hint against it, for the various reason you suggest, then it belongs in a http-middleware-utils package, not in the PSR package.

mindplay-dk commented 7 years ago

@atanvarno69 middleware can type-hint against it statically, with doc-blocks.

There may even be situations (maybe pipes) where someone wants to statically type-hint against it, that's really up to implementors.

Including this interface in the spec ensures that implementors who type-hint against it statically, even with a doc-block, are type-hinting against the same interface.

There is zero benefit to moving this to a separate package, just more complexity (version management, etc.) and including it is zero overhead.

Think of it as a formalization of the specification - though as said, in some cases, someone might want to type-hint against it.

schnittstabil commented 7 years ago

delegate providers could just as easily not code against DelegateInterface

I see your point, but I do not agree about the purpose of a PSR: It is abbreviation of PHP Standards Recommendation and if you take a look at PSR-7, then you will see: it's full of key words like 'SHOULD', 'SHOULD NOT', 'RECOMMENDED' and 'OPTIONAL' (not every time capitalized).

Thus, in my opinion it is absolutely valid to provide that interface…

atanvarno69 commented 7 years ago

@mindplay-dk, @schnittstabil I see I am in a minority on that point, so I will accept what you say on move on.

Just for my own clarity, will the final PSR say that a PSR-15 compliant delegate MUST, SHOULD or is RECOMMENDED to implement DelegateInterface, or leave it entirely as an implementation detail?

schnittstabil commented 7 years ago

Just for my own clarity, will the final PSR say that a PSR-15 compliant delegate MUST, SHOULD…

That is open to debate, see #41, if we agree on callable at all – And personally, I welcome every of your comments – even if we have different opinions – especially when they are different.

mindplay-dk commented 7 years ago

will the final PSR say that a PSR-15 compliant delegate MUST, SHOULD or is RECOMMENDED to implement DelegateInterface

If the static type-hint is callable, it follows the method-name of the delegate needs to be __invoke(), at which point it's automatically a MAY.

If it's type-hinted in the interface, there won't be any choice, it'll be a MUST.

We'll see how this pans out.

shadowhand commented 7 years ago

I see two separate issues here:

  1. Changing Delegate::process() to Delegate::__invoke()
  2. Changing the $delegate type hint from DelegateInterface to callable|DelegateInterface

I'm certainly in favor of the first and not sure about the second. The problem with callable type hint is that:

Down the road, if a PHP 7 version of the interfaces were created, it would not be possible to provide strict typing, because a generic callable cannot specify the return type. Type hinting against DelegateInterface ensures this:

class DelegateInterface
{
    public function __invoke(ServerRequestInterface $request): ResponseInterface
}
schnittstabil commented 7 years ago

Down the road, if a PHP 7 version of the interfaces were created

As far as I know, they will be in a new namespace, at least I believe they must have new names because of BC issues. Furthermore, I think we can do the same as mentioned at https://github.com/http-interop/http-middleware/pull/41#issuecomment-264167423 if the time comes.