MAX POWER
MAX POWER

Reputation: 5458

Laravel 8 - Run Route Middleware before Constructor

I am using Laravel v8.35. I have created a middleware EnsureTokenIsValid and registered it in app/Http/Kernel.php:

protected $routeMiddleware = [
    ...
    'valid.token' => \App\Http\Middleware\EnsureTokenIsValid::class,
];

Here is the middleware itself:

<?php

namespace App\Http\Middleware;

use Closure;

class EnsureTokenIsValid
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->input('token') !== 'my-secret-token') {
            return redirect('home');
        }

        return $next($request);
    }
}

Essentially this middleware will redirect the user to a login page if the token is not valid. Now I want this middleware to run on specific routes. So I tried doing this:

Route::get('/', [IndexController::class, 'index'])->middleware('valid.token');

However it seems the code in the constructor of the parent controller (app/Http/Controllers/Controller.php) is being called first. My controllers all extend from this parent controller, e.g:

class IndexController extends Controller

I have tried putting the middleware at the very beginning in the constructor of Controller.php, but that does not work either, i.e. it just proceeds to the next line without performing the redirect:

public function __construct()
{
    $this->middleware('valid.token');

    // code here which should not run if the above middleware performs a redirect
    $this->userData = session()->get('userData');
    
    // Error occurs here if 'userData' is null
    if ($this->userData->username) {
        // do stuff here
    }
}

If I put the middleware in my IndexController constructor, it works. However I don't want to do this for every controller - I just want the middleware to exist in the parent controller.

Upvotes: 0

Views: 2124

Answers (2)

Christopher Thomas
Christopher Thomas

Reputation: 4723

So, I've also run into this problem and from debugging through the laravel framework code. It runs all the global middleware, then gathers the router middleware, constructs the controller, then afterwards runs all the middleware from the router + all the controller middleware configured in the controller constructor.

I personally think this is a bug, but that doesn't really help you since you need a solution and not just complaining.

Basically, your route no doubt targets a method on the controller, put all your dependencies into that function call and any code that relies upon it into that function call too.

If you need to share a common set of code which runs for each method in that controller, just create a private method and call it from each of the methods.

My problem was that I was using the constructor for dependency injection, like we are all expected to do, since a fully constructed object should have all it's dependencies resolved so you don't end up in a half constructed object state where depending on the function calls, depends on whether you have all the dependencies or not. Which is bad.

However, controller methods are a little different than what you'd consider a typical object or service. They are effectively called as endpoints. So perhaps it's acceptable, in a roundabout way, to consider them not like functions of an object. But using (abusing perhaps), PHP classes to group together methods of related functionality merely for convenience because you can autoload PHP classes, but not PHP functions.

Therefore, maybe the better way to think about this is to be a little permissive about what we would typically do with object construction.

Controller methods, are effectively callbacks for the router to trigger when a router is hit. The fact that they are in an object is for convenience because of autoloading. Therefore we should not treat the constructor in the same way we might for a service. But treat each controller endpoint method as the constructor itself and ignore the constructor except for some situations where you know you can do certain things safely.

But in all other cases, you can't use the constructor in the normal way, because of how the framework executes. So therefore we have to make this little accommodation.

I think it's a bug and personally I think it should be fixed. Maybe it will. But for today, with laravel 9, it's still working like this and I think this at least will help to guide people who ran into the same problem.

Upvotes: 2

lagbox
lagbox

Reputation: 50561

If you have the web middleware group assigned to this route it doesn't have access to the session in the constructor of your controller any way. You will need to use another middleware or a closure based middleware in the constructor so that it will run in the middleware stack, not when the constructor is ran:

protected $userData;

public function __construct(...)
{
    ...

    $this->middleware(function ($request, $next) {
        $this->userData = session()->get('userData');

        if ($this->userData && $this->userData->username) {
            // not sure what you need to be doing
        }

        // let the request continue through the stack
        return $next($request);
    });
}

Upvotes: 2

Related Questions