Laravel

Policies and Route::apiResource in Laravel

Published September 25th, 2020

While I’ve been a PHP developer for 20 years, Laravel is a new area for me, and I must say, am as happy as the proverbial pig. But I did encounter a really head-scratching quirk when working with the authorizeResource call from a Controller.

So here’s the back story… I have a Model called UserDelegation (where a User can delegate responsibilities to another User for a period of time), and that in turn has an associated FormRequest, Resource, Policy and pair of Controllers – one for admin, and one for normal users – you know, all the good practice components.

When creating the UserResource migration, the necessary Permissions are added to the database too so that I can include logic in my UserDelegationPolicy. All hunky dory so far.

In my api.php file, I am adding my routes as an API resource:

Route::apiResource('delegations', 'Manager\UserDelegationController');

When Laravel defines my routes, I am getting the expected GET, POST, DELETE and PUT endpoints for the “delegations” route – which is excellent.

In my controller, in its constructor, am calling authorizeResource

public function __construct()
{
    $this->authorizeResource(UserDelegation::class);
}

But when I try to access the endpoints, I receive a 403 error. Why? My other policies for other Models, such as “Title” or “Organisation” are working as expected. My Policy is using the correct permissions for the new Model, so what gives? Head scratching moment, and Google wasn’t helping, so time to explore the Laravel source to get a better understanding of what is going on.

Looking at the authorizeResource call, this accepts multiple arguments, starting with the Model’s class name. Makes sense so far. But also more arguments, the second called $parameter, which I had never actually used.

This is where the issue stems from, and is definitely a user error.

If you do not include $parameter, the authorizeRequest function will create a snake-case $parameter based on the base name of the Model – so in this instance, it would set $parameter to be “user_delegation”.

However, in my route definitions, Laravel has defined the route parameter as “delegation”, creating a singular representation of my route name. What I mean is that my route looks like:

api/delegations/{delegation}

But you see that authorizeRequest, without explicitly setting $parameter, has “guessed” that it would be “user_delegation”. This means that it would fire for a route like:

api/delegations/{user_delegation}

When I try to call my endpoint, it is not matching because my route is defined with “delegation” but Laravel’s authorisation is expecting “user_delegation”.

How to fix this… well there are two ways to approach this:

  • Explicitly define $parameter with the authorizeResource call

  • Explicitly define the parameter used in the route definition

Be explicit with in the controller

Quite simply, when authorising your Controller, explicitly give it the parameter to look for in the route.

public function __construct()
{
    $this->authorizeResource(UserDelegation::class, 'delegation');
}

While Laravel’s documentation does include reference to this, which I eventually found, to me it didn’t read as obvious as it may be should have. But when you stop and re-read it, it does tell you exactly what you need to know.

Be explicit with in the route definition

This one is a little more wordy to prepare, and means you’re adding the $options array to your route definition, and passing the parameters array with the definition that “delegation” should actually be “user_delegation”.

Route::apiResource('delegations', 'Manager\UserDelegationController', [
    'parameters' => [
        'delegation' => 'user_delegation'
    ]
]);

This makes sense too, and could be useful if you have a really different approach to naming conventions, but does make the api.php routes file less readable (if you ask me). I love the simplicity of my routes file with such clean and easy-to-scan apiResource calls.

Which is better?

Well… I’m undecided – both solve the problem, but as to “better”, that might be more of a coding style question for the project. What do you think? What solution reads better?

I feel that the authorizeResource solution may be easier to write, and a better habit to have, by being explicity with every single authorizeResource call simply passing the $parameter. That seems a simpler best-practice with an easier-to-read api.php routes file. But happy to hear your thoughts too.

You may be interested in...