New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PoC][WiP] callables #39
Conversation
💣 EXPERIMENTAL –
|
interface DelegateInterface
{
public function process(RequestInterface $request);
}
interface ServerDelegateInterface
{
public function process(ServerRequestInterface $request);
} Possibly related resources:
Done, see #40 |
Aura.Router already supports
|
There's way too much going on this PR. If the goal is to change the middleware signature to use a Also, I am not sure what the point of |
build/ | ||
vendor/ | ||
composer.lock | ||
composer.phar |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is all of this being added? http-middleware
contains only interfaces and will never be testable, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR is not meant to get merged:
This is not a serious PR, it is only a proof of concept – I've just found some time to investigate the idea of describing middlewares and delegates by
callable
andinterface
and what we might lose.
I'm willing to transfer this stuff to http-interop and waive all copyright rights, so frameworks can safely use it or could create their own implementations based on this work. However, I would suggest to create a new project for that…
It was the easiest way to fiddle around and it allows us to discuss that here. If you think it would help, then I will create a new PR, only with the delegate stuff...
@@ -0,0 +1,17 @@ | |||
<phpunit bootstrap="./vendor/autoload.php" colors="true"> | |||
<testsuites> | |||
<testsuite name="Zend\\Stratigility Tests"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😕
@@ -8,11 +8,11 @@ | |||
interface DelegateInterface | |||
{ | |||
/** | |||
* Dispatch the next available middleware and return the response. | |||
* Process a request and return the response. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This commentary is still not correct. A delegate is deferring processing to the next available middleware. It is not, in and of itself, processing anything.
@@ -9,12 +9,12 @@ | |||
{ | |||
/** | |||
* Process an incoming server request and return a response, optionally delegating | |||
* to the next middleware component to create the response. | |||
* the response utilizing $delegate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't delegate the response.
use ReflectionFunctionAbstract; | ||
use TRex\Reflection\CallableReflection; | ||
|
||
if (! function_exists('Interop\Http\Middleware\is_delegate')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not necessary. The only time we needed to use function_exists
before defining functions is when namespaces were not available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, function_exists
would have been necessary. See for example nikic/FastRoute#39
@@ -20,16 +20,49 @@ | |||
], | |||
"require": { | |||
"php": ">=5.3.0", | |||
"psr/http-message": "^1.0" | |||
"psr/http-message": "^1.0", | |||
"raphhh/trex-reflection": "^0.2.3" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not in favor of adding a global dependency on a reflection proxy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could do it manually, but this would mean to create a function or class somewhere – or it would not get DRY. I'm not aware of any other way to share code which wouldn't get public 😒
For /**
* @param DelegateInterface|callable $delegate
*/ I rarely use full featured IDEs, but the last time I've used them, I was able to navigate to the
In summary: I can read its contract. Furthermore, there are some developers who cannot write code without their beloved IDE and will moan if they don't have any kind of IDE support 😉 For middleware container implementors: class Delegate implements DelegateInterface
{
//…
/**
* {@inheritDoc}
*/
public function __invoke(RequestInterface $request)
// …
} Because I use the
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 and it can be used to call any For the PSR-15 and its meta documentation:
|
-1 for the proposal from me (will give more detailed context later today), on top of my head:
|
@DaGhostman Probably, I've written to much and some points were drowned in this flood of information:
PHPStorm might be the very best PHP-IDE in the whole universe – But I still don't need it, it takes hours to load and encourages bad coding. But that is my personal opinion and I don't try to convert you to become better programmer – Why do you try to force me to use your coding patterns? I don't like them!
The LOC does not bother me much – The issue is: the amount of classes, the code needed to inject dependencies into them, and the adapters (e.g. see
That is your opinion and depends on your tools. I use different tools, TDD/BDD and CI for example – I haven't used debuggers for years and I don't miss them at all. Anyway, do you think debugging 3 different classes at 3 different locations is easier than debugging 1 single class?
If you mock an public function test()
{
$middleware = …;
$request = …;
$count = 0;
$response = $middleware($request, function (RequestInterface $res) use (&$count) {
$count++;
$this->assertSame(…, …);
return (new Response())->with…;
});
$this->assertSame(1, $count);
} Furthermore, If you must use mocks and stubs to test your code, then your design is flawed: you have written code which is not reusable. In my example above, I don't need a mocking library, the callable is a valid delegate, which could be reused some where else.
I really hope I misunderstood you. Experienced developers call that Information hiding, program to interfaces, not implementations and especially Loose coupling:
Hence, it is a feature – not a bug.
That doesn't help much. I don't remember when And if you don't want them, then don't use them, just stick with the interfaces |
@schnittstabil, with the dual type-hint in the comment or type-hinting against On the IDE/Editors part, sorry but unless you are writing your code with pen and paper, every CODE editor I know/have used is binding The argument about code being written by everyone is kind of pointless, imo. Why do we always have to reinvent the wheel not everyone will have to rewrite it(I don't see the point personally but that is beyond the point of this discussion), like many (including myself) are using Because symfony did not implement PSR7 in 3.x, iirc the controversy about PSR7 was still going on, now laravel/lumen use that, cuz... reasons and that PSR lacks adoption, whereas it clearly is a step in the right direction. Well I will not agree fully on the mocking part, excuse me, but if you want to get a mock out of a interface - since you have type-hinted against said interface - you kinda have to (I avoid using idk, why do you refer to the debugging as using an actual debugger, since
See also Debugging Techniques I most of the time like to have a meaningful error messages, with a more informative trace, than a series of The point about information hiding, is not exactly on point in this context since this is a PSR, which will become a standard, which will not change, hence different implementations should behave the same way. SRP between the delegate and the middleware is preserved, so the middleware must not care about the delegate as long as the interface contract is truthful. Again loose coupling, is still not applicable, since this is a standard, it is not likely to change in the near future. You are looking to decouple what from what exactly? Remove the contracts?
Well, I've picked up PHP from around 4->5 era and got serious with it in the early 5.3 days. All I am saying is that, Also I can't think of any other OO language that uses a lot of functions or any at all, except for JS, which imo is the reason everyone is so hyped about TypeScript, since it brings classes, interfaces, types, etc. to the table. P.S |
@DaGhostman I'm sorry, I will try to improve. On the one hand, you said that (almost?) everybody uses a code editor which provides code completion, and on the other hand you said type-hinting against About your type-checking concerns:
About your PHP fragmentation argument: If PSR is not about interoperability, why should we standardize middlewares at all? I mean, is this only about all the big players? Should PSR-15 for middleware (stack/pipe/…) implmentors be less useful, because Symfony/Zend/Laravel/… would benefit from the lack of competition? Maybe you could clarify the following, I don't really understand what you mean:
I don't want to dive into a debate about how to write good code or similar. I just want to clarify my statements above. I test first – always – that is, if I encounter a bug, the very first thing I do is writing a test to reconstruct the problem, thus TDD/BDD is my way to debug and I do not need phpdbg or an IDE for that. And I'm not alone, many developers have made the same experience. I agree, that this is a standard and should be fully dedicated to the needs of the middleware context. But I don't understand why you all want to stick so much on this contract: this interface represents a delegate of a middleware container, and it is useless in any other context of middlewares. I have presented my point of view at #35. I don't say: lets reuse the About functions in other languages: functional programming languages are gaining more and more ground (JavaScript is one of them) these days. But also all big OOP languages introduce more and more functional programming concepts: C#, Java and even C++! And I foresee that PHP will do the same. I believe, that sticking too much at OOP-only fails to appreciate current development trends. |
I would like to present a further argument. Lets take look at the ideas of most big players, I'm aware of. They all came up with modern, lean micro frameworks: // Slim Framework Team: Slim
$app->get('/hello/{name}', function ($request, $response, $args) {…});
// Sensiolabs: Silex
$app->get('/hello/{name}', function ($name) use($app) {…});
// Laravel: Lumen
$app->get('user/{id}', function ($id) {…});
// Zend: Expressive
$app->get('/path', function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {…}); Lets take a more general look at them:
I try to find some time today, to take a deeper look at your anonymous functions produce hard to read stack traces argument, but PSR-15 targets all of these frameworks and all of them encourage the usage of anonymous functions, thus I do not believe it is a real problem. The only, almost valid argument against
This means, the Zend guys decided to use an The suggestion, I present in this PR hopefully shows that BC issues can handled by the PSR-15 contract and run-time checks. Furthermore we would gain flexibility and resolve every, I mean really every possible name conflict with already existing I don't know, which arguments against |
I think this arguments is mostly about preference, code style, et al, hence making it inappropriate on the PR. That being said, I am kinda against using the That said, I find myself advocating a lot lately that everything is middleware (tm), even controllers, enforcing OOP that (as my understanding goes) is that when applying composition you end up with how zf expressive uses it's route definitions/configs. My point against type-hinting against callable is that: You cannot enforce anything when you use
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). A framework(lets all agree that we are on the same page and we are talking about OOP frameworks, regardles nano, micro or otherwise) imho should imply/enforce good practices and if you and I, as experienced developers will comply with the interface, many newer devs will not, since the new hype is using closures instead of classes, project grows and things tend to go sideways in terms of SOLID. Those closures will - at some point - need to become classes, since they have grown to 20-30 lines and helper methods should be used applied to keep things readable In my opinion this is a reason enough to keep away from it, I will prefer any descriptive code, which at first glimpse will give me a rough idea what it is doing, rather than code that will end up looking like clojure/lisp (i picked PHP you know). I am rising my concerns since I believe that the idea behind PSRs i good, not always likable at first (think PSR7 no likable), but on the long run it pays. There should be a discussion as this is not something to be taken lightly. Now the hype is to lambdas, Sorry I couldn't help, but post this. Kinda illustrates my point and it exists, so it is not such an imaginary problem |
As long as we talk about fat frameworks, I fully agree: You want SOLID, classes and all that stuff. But those fat frameworks are really, really difficult to learn for novices, with all the conventions and configuration files; and additionally most of them are hard to setup for them. But most worst: a good pattern in one framework is considered bad practice in the next one. Personally, I want to start with a simple solution and if time comes I want to switch smoothly to a more powerful one – and middlewares and delegates are perfect for that: // kickoff (contact.php)
$stack = new Stack(function (ServerRequestInterface $request, callable $next) {
// send email…
});
$stack->push(…);
$stack(new ServerRequest());
// Sometimes later, I can switch to a mirco framework – and even later,
// when I need DI and all that stuff, I can switch to a fat framework
class ContactPostAction implements ServerMiddlwareInterface {…} // and even later, you may want to reuse your `ContactPostAction` company-wide with:
class BasicMiddlewarePipe implements ServerMiddlwareInterface {
private $pipe;
public function __construct(ServerMiddlwareInterface $action)
{
$this->pipe = new MiddlewarePipe([
new Middlewares\CsrfProtection(),
// whatever…
$action
]);
}
public function __invoke(ServerRequestInterface $request, callable $next)
{
return ($this->pipe)($request, $next);
}
}
// composer require foo-company/contact-action foo-company/basic-middleware-pipe
// and just use it in a micro-framework/fat framework or standalone:
$app->get('/contact', new BasicMiddlewarePipe(new ContactPostAction())); Take that contact example: in many, many use cases you don't need DI, caching and whatever: neither today nor later – micro frameworks exist exactly because of that: they are fast and easy to set up: just create a single tl;tr If we do not have You may say, it is bad practice anyway – I say, an anonymous function fits best in such simple use cases.
@DaGhostman I would thank you, that you gave me more details about your type-hint to
The interfaces would look like: interface DelegateInterface
{
/**
* Dispatch the next available middleware and return the response.
*
* @param RequestInterface $request
*
* @return ResponseInterface
*/
public function __invoke(RequestInterface $request):ResponseInterface;
}
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 DelegateInterface $delegate
*
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, DelegateInterface $delegate):ResponseInterface;
}
interface Delegate5Interface
{
/**
* Dispatch the next available middleware and return the response.
*
* @param RequestInterface $request
*
* @return ResponseInterface
*/
public function __invoke(RequestInterface $request);
}
interface ServerMiddleware5Interface
{
/**
* Process an incoming server request and return a response, optionally delegating
* the request utilizing $delegate.
*
* @param ServerRequestInterface $request
* @param Delegate5Interface $delegate
*
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, Delegate5Interface $delegate);
}
Of course, the constraints are debatable, especially the MUST/SHOULD/MAY/etc. This would allow us to support many, many use cases from simple to complex ones.
Please note: not only the 1. advantage relies on run-time checks, the 2. and 3. do as well. if ($m instanceof ServerMiddlewareInterface || $m instanceof ServerMiddleware5Interface) {…} EDIT: EDIT2: Extended contact-usage example. |
I can't keep up with this thread, but this IDE hate I'm hearing? Stop that. Type-hints are not only about IDE support, they're about support for any static analysis tools, including offline QA tools like CodeSniffer and MessDetector. Yes, that also happens to give us IDE support. Your personal feelings about IDEs have no place in this thread. |
You're making some rather broad assumptions that are, frankly, incorrect. We added our Additionally, the problem is not with the
The problem with having
It's having the second argument optional that presents a problem. If http-interop middleware calls on the delegate, and passes only the request, but the next in the stack is double-pass middleware, how does it get the response? Additionally, we need to check the signature of the next middleware in the stack to see what type of middleware it is, in order to determine what arguments to pass to it. These are all solvable, but they're far easier to solve when the methods differ. Regarding having If I compose one middleware inside of another: class SomeMiddleware
{
public function __invoke($request, $delegate)
{
}
}
class CompositeMiddleware
{
private $nested;
public function __construct(callable $middleware)
{
$this->nested = $middleware
}
public function __invoke($request, $delegate)
{
// call the nested middleware as part of our work...
}
} The above leads to some interesting gymnastics, based on the PHP version you're using. Under PHP 5, you need to do the following: $nested = $this->nested;
$response = $nested($request, $delegate); and, in point of fact, that may not work if the nested middleware is a callable of the form Under PHP 7, you can do this: $response = ($this->nested)($request, $delegate); In every case, having a defined method, instead of relying on the $response = $this->nested->process($request, $delegate); Not using |
I am closing this out, as per other issues/PRs on this topic. Callable type hint reduces runtime safety and static analysis. |
Sorry, for that. But many other implementers use
@weierophinney Are you concerned about that 2 year old bug? https://bugs.php.net/bug.php?id=68475 Anyway, supporting both types of middlewares side-by-side by implementing both interfaces, is a design mistake in my opinion and will lead to hard to debug errors: #44 (comment) @shadowhand Thanks for closing. I see that a PSR should not encourage |
FYI, comparing this PR with PHP 7.1
Well, the second point is clearly unfortunate. But I vehement protest against the validity of concerns @DaGhostman and others have mentioned, who believe that this approach does not fit well in the PHP world. It's obviously just the modern way how standards deal with missing language features (like typesafe callable). In addition, PHP 7.1
Thus, as far as I can see, if |
Comparing iterator and middleware is flawed. An iterator can (for the most part) be used as an array. This is, without question, not the same thing as a middleware being a callable. |
Well, it seems that you do not often use
That's not the point and doesn't matter here. If you reread my #39 (comment), then you will see that I'm talking about the concerns about the impact of this solution on the PHP ecosystem (see the FIG Mission Statement, why this have to be considered). |
The
It allows accepting multiple types, rather than one strict type. |
WUT? We had a similar debate about
Exactly the same holds for middlewares, which is due to the fact, that this PSR clearly enforces:
|
@shadowhand For clarification, if we consider the following interfaces: interface DelegateInterface
{
public function __invoke(ServerRequestInterface $request);
}
interface MiddlewareInterface
{
public function __invoke(ServerRequestInterface $request, DelegateInterface $delegate);
} Then $m = function(ServerRequestInterface $request, DelegateInterface $delegate) {…};
var_dump($m instanceof MiddlewareInterface); // => false |
@DaGhostman, @atanvarno69 and anybody else who's interested. While this debate is already over, @DaGhostman's #34 (comment) showed me that there are open questions. @DaGhostman None of the SOLID concerns you've mentioned (neither above nor anywhere else around here) make sense. If you still do not know Why, then please read up their definition and ask. I'm willing to answer all of your questions. |
Btw, I also have a question regarding to this:
@DaGhostman Is not the only one who believes that you cannot enforce anything. I mean, if I type-hint against function foo(callable $bar) {
$bar();
} Hence, why did @DaGhostman (and others) enumerate valid |
This is not a serious PR, it is only a proof of concept – I've just found some time to investigate the idea of describing middlewares and delegates by
callable
andinterface
and what we might lose.I hope this stuff will help to pursue the debate about that topic, so everybody can rethink his stand based on a more concrete level.
If you just chimed in
#37 is about the following change:
I know, we are discussing the
process
to__invoke
change only for theDelegateInterface
at #37 yet, if that bothers you, then just read over theServerMiddlewareInterface::__invoke
parts.The idea: The interfaces are used to describe the signature and the behavior which the
callables
must fulfill. But moreover, the interfaces MAY be used by implementers, hence they get code completion and static type analysis, at least to some extend.Possible Drawbacks
BCs and migration problems about
__invoke
were discussed in passionate debates. The main problem: Frameworks may already have introduced interfaces with__invoke
but with different signature/contract, notably Zend\Stratigility.Two ideas alleviate that problem:
callable
s, implementers can create a new PSR compatible middleware method:About this PR
Personally, I wasn't sure if type checking at run-time is powerful enough at PHP 5.4+ for our purposes.
So I gave it a shot and created two functions:
I believe they are thoroughly tested: I have written many delegate and middleware implementations only to document which delegates/middlewares are considered as valid and which are not. You may scan over the
tests/Fixtures/*/{Valid,Invalid}
directories – in my opinion they all work as expected.The most interesting observations I've made:
tests/Fixtures/Delegates/Invalid/MagicCall.php
shows that__call
cannot be verified – very obvioustests/Fixtures/Middlewares/Invalid/PureInvokableWithDelegate.php
shows that we lose the ability to have(ServerRequestInterface $request, DelegateInterface $delegate)
middlewares – we may allow that as well, but that's not how interfaces work and would lead to many problems.tests/Fixtures/Delegates/Valid/functions.php
shows that linters and IDEs will annoy implementers with unused formal parameters warnings if they don't use all parameters, notably PHPMD. Btw, it is currently not possible to suppress the warning in that file, which is related to SuppressWarnings annotation for a function doesn't work phpmd/phpmd#337.tests/Fixtures/Middlewares/Invalid/PureInvokableDoublePass.php
and many other tests show that we can safely distinguish double-pass from single-pass.I'm willing to transfer this stuff to http-interop and waive all copyright rights, so frameworks can safely use it or could create their own implementations based on this work. However, I would suggest to create a new project for that: PHP may eventually introduce new types of
callables
and after all it is still in an early stage…Whether we chose the
callables+interface
style for middlewares or delegates or both or not at all, I'll be fine with it – As you perhaps know, the contract (the naming and the descriptions in the comments) is much more important to me…Best regards
Michael