Closed mindplay-dk closed 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?
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…
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.
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…
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.
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…
@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.
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:
$delegate($request)
makes sense too@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…
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.
I like the idea more and more.
Let's say we do the same with ServerMiddlewareInterface
:
A middleware is a
callable
, which fulfills theServerMiddlewareInterface
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']);
@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.
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.
If the delegate is type hinted
callable
rather thanDelegateInterface
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 aResponseInterface
,$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…
@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
@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:
"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.
"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.
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).
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:
__invoke(RequestInterface $request):ResponseInterface
is_delegate
function will check the return type as wellThus, 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.
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)
{
}
}
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
vscallable
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 toServerMiddlewareInterface::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:
ServerMiddlewareInterface
method should be DelegateInterface
not callable
.DelegateInterface
method __invoke()
rather than process()
. However, after considering the points raised, I do not think it would be a big deal if __invoke()
were preferred. If the wise of FIG opt for that, then that is fine.I think I should self-throttle for a while and let others chime in.
@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…
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.
Re: the reply on the hypothetical future
ExtendedDelegateInterface
, my point was that type hinting callable in theServerMiddlewareInterface
method precludes future extensions toDelegateInterface
…
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) {…}
}
}
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 class – callable
FTW!
(All LOC were calculated by https://insight.sensiolabs.com)
@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...
@shadowhand updated
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.
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 __invoke
able, 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:
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.
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...
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:
@internal
mindplay/middleman
.Beside that this was the whole new point, I just wanted to improve your current situation as well as for others…
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.
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.
: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);
},
// …
];
@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 :-)
@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?
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.
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.
@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
.
@schnittstabil Yes, and I agree with the counter points provided by @DaGhostman (this, as well as his various other replies).
@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.
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 But I agree, with this approach, the current parser (QA or PHP itself) will ignore that comment.DelegateInterface
.
Maybe you are wondering why I capitalize those words: Key words for use in RFCs to Indicate Requirement Levels.
@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 usingcallable
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).
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.
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.
@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.
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…
@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?
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.
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.
I see two separate issues here:
Delegate::process()
to Delegate::__invoke()
$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
}
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.
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 ofDelegateInterface
that simply wraps acallable
, 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: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 plaincallable
.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?