May 25, 2020

Don't always pass the ability when authorizing some action in Laravel

You know you can authorize any action in the controller via authorize method where you need to pass the ability as the first argument and the model as the second one.

But when you don't pass the ability to that method, Laravel will assume the ability from the controller method name. So in this example:

public function update(Post $post)
{          
    $this->authorize('update', $post);
}

you can omit the Policy method name as it's the same as the controller method name where you call the authorize from:

public function update(Post $post)
{          
    $this->authorize($post);
}  

How cool is that? But...


How is it even working?

Things get more clear when you take a look at the AuthorizesRequests trait which is imported in the App\Http\Controllers\Controller which means in every controller that extends this class. Here is the authorize method:

public function authorize($ability, $arguments = [])
{
    [$ability, $arguments] = $this->parseAbilityAndArguments($ability, $arguments);

    return app(Gate::class)->authorize($ability, $arguments);
}

$ability and $arguments variables are going from the parseAbilityAndArguments method:

protected function parseAbilityAndArguments($ability, $arguments)
{
    if (is_string($ability) && strpos($ability, '\\') === false) {
        return [$ability, $arguments];
    }

    $method = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];

    return [$this->normalizeGuessedAbilityName($method), $ability];
}

It immediately returns $ability and $arguments variables if we explicitly pass an ability: $ability is actually a string and doesn't contain \\ so it's not a classpath. Here is the fun part:

You can see the usage of debug_backtrace function where all the magic happens. It is a usual PHP function that can show us what has been called so far as an array. The second argument tells how many calls we need to know. It's 3. First is the current function, second is the authorize method and the third one is the controller method itself. So we need to grab the function name of the last one. There we go! That's how Laravel assumes the ability name.