-
Notifications
You must be signed in to change notification settings - Fork 11k
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
[9.x] Do not instantiate the controller just for fetching the middleware #40397
Conversation
Can you describe what this actually does? You talk a lot about the previous PR but not much about this one. |
Sure, updated the description. |
I guess I don't understand what this is solving. What does this allow people to do? If I don't want my controller to define middleware after being constructed, what do I actually do in my code? Can you give examples of real code please? |
Alright I see, it all boils down to the following: Take this example, it has a
With current behavior the constructor injected Thus with this change both |
1- It aims to the situations in which people do In other words, they do not want to bind in the 2- It also gives the opportunity to use multiple implementations for a single controller. Example:
public function handle($request, Closure $next) // <------- the 'binder_11' middleware
{
app()->bind(SomeInterface::class, MyClass11::class)
return $next($request);
}
public function handle($request, Closure $next) // <------- the 'binder_22' middleware
{
app()->bind(SomeInterface::class, MyClass22::class)
return $next($request);
} The Controller: class ApiController
{
function __construct(SomeInterface $obj) // <---- errors out, since app()->bind() is in middleware
{
// ...
}
// ...
} Now, middlewares determine the injected class: Route::get('/fooo', ApiController::class)->middleware('binder_11');
Route::get('/baar', ApiController::class)->middleware('binder_22'); Here if the user visits |
The problems that this PR solves are described in #40306 (comment) The difference with this PR as opposed to the last one is that the end users wouldn't have to change their controllers because the middleware registration syntax remains the same. If users extend |
I know this has been merged but I am a bit concerned about this PR and there are a few points I'd like to mention.
|
@lorisleiva Yes, there's now that condition upon which it depends whether the controller will be instantiated immediately during the fetching middleware phase or only when it's actually needed. This solution was accepted in the end because it does not have any breaking change as opposed to #40306. If you are aware of any concrete scenario where this would break something please describe it in more details, I currently do not see such a case, the controller route caching logic remained the same as before.
That is just the thing, |
It is quite unfortunate that its based on the
I fail to see how a controller can be constructed twice in a single request lifecycle? And the controller was already being cached before, thus your notion of "the __construct not being called again" doesn't seem valid as it was only being called once before this PR anyways, it was just cached before middleware were ran VS after middleware being ran.
As this follows up on the point above, i don't believe this will break any Octane applications as they would have already been broken in such case. (not 100% versed in octane so i could just as well be wrong here)
You could, indeed, except me and many others have been feeling weirdly about that since we're expecting injection to work like it does for any other class. |
Thanks for your reply guys. I'm not saying that we were constructing the controller twice before the PR nor that we should. What I'm saying is that, by allowing us to write application logic within the Take a look at the following diagram. Before, it was clear that Now, the first request has a different behaviour than the subsequent ones. Middleware will be executed before the controller is resolved in the first request but after the controller is resolved in the subsequent requests. Take a look at the following example. // "binder" middleware.
public function handle($request, Closure $next)
{
if ($request->get('status') === 'pending') {
app()->bind(SomeInterface::class, SomePendingClass::class);
} else {
app()->bind(SomeInterface::class, SomeReadyClass::class);
}
return $next($request);
}
class ApiController
{
function __construct(SomeInterface $obj)
{
// ...
}
}
Route::get('/api/{status}', ApiController::class)->middleware('binder');
// GET /api/pending => $obj is instance of SomePendingClass::class.
// GET /api/ready => $obj is still an instance of SomePendingClass::class when application is kept alive. I'm not saying this would have worked before this PR, but I'm worried that encouraging developers to add application logic and dependency injections within the Finally, note that this is not about "code style". I also very much like the "construct + invoke" pattern in which the former registers dependencies and the latter executes the action. I just wanted to express my concern with regard to this change of behaviour which will likely cause unpredictability and confusion. |
Thanks for the clear explanation, i can definitely understand what you're getting at and how this could be an issue in environments running with octane. I suppose the only way to fix the issue you describe is to either prevent the controller caching, or remove the cached controller after it's dispatched. It seems the whole reason this caching exists anyways is to prevent double resolving said controller when middlewares are collected. In my opinion this is a by-product of using coroutines and something that the developer has to keep in mind as this might just as well happen in user defined classes as well. But i'm all for preventing shipping the framework with said problem in its routing. |
While I support the general change, perhaps specifically in the form of @X-Coder264's implementation, I agree with @lorisleiva. In my opinion, the ideal behavior would:
If you agree with those two points, then I think there's no implementation that'd work with middleware specified in This is also why I think @X-Coder264's implementation was the best one. 99.99%+ of users won't be affected in any way by changing Perhaps that's the solution we could re-try for v10 if the Laravel team agrees that my two points above have merit. I'd be interested in @lorisleiva's thoughts on that implementation as well, since he provided very useful context here. |
This PR is a less intrusive submission of #40306
Summary of what this intends to fix:
Laravel offers the option to have middleware definitions in the controller constructor via the
->middleware()
method.This feature constructs the controller before the middleware are ran and we are therefore unable to inject classes that are bound in a middleware or of which the driver is selected in the middleware (
Guard
implementation for example See an example here).Previous PR was super intrusive as it changed the current behavior for everyone, this PR aims to keep current behavior as is, but fixes the issue that me and others are running into.
The only downside is a new interface method, but i would argue that this is an interface that is rarely overridden.
How it works
Previously a controller was constructed and then passed to the
ControllerDispatcher@getMiddleware()
method to retrieve the middleware defined in the dispatched controller. In there amethod_exists
resides to make sure the given Controller has agetMiddleware
method.This PR moves the
method_exists
to a method on theControllerDispatcher
and verifies if middlewares should be collected. If so it does what it always did. If not then the instantiation is delayed until controller method is ran, because of this the middlewares are constructed first, and then the Controller. Instead of the other way around.ping @X-Coder264