Use Morph Map names to resolve Model for Laravel Gate Policies

Reece May • July 6, 2020 • 4 min read

laravel tutorial

When using the Blade directives in Laravel for authorizing resources and actions such as the @can and @cannot you have to place the classes full path into the function, eg @can(\App\User::class) or @can('App\User').

I had a cunning plan to extend the way the Gate class resolves the models in the same way as it does with Policy classes.

Laravel gives a nice way to extend the way it finds the Policy class for a model by using the Gate::guessPolicyNamesUsing() function.

The entire solution doesn't require much and allows you to add it in without causing issues.

After that you would be able to write @can('viewAny', 'users') and that would resolve to App\User or App\Models\Users

Creating the new Gate class

The first file that you would want to create is the CustomGate class. You can create it anywhere you prefer.

The class extends the normal Gate to allow normal usage.

⚠️ Important, we must ensure that it implements the Illuminate\Contracts\Auth\Access\Gate contract.

Create a file in the app\Extensions directory and call it CustomGate.php.

In that file we are going to place the following.

<?php

namespace App\Extensions;

use Illuminate\Auth\Access\Gate;
use Illuminate\Contracts\Auth\Access\Gate as AccessGate;

class CustomGate extends Gate implements AccessGate
{
    /**
     * The callback to be used to guess policy models.
     *
     * @var callable|null
     */
    protected $guessClassNameUsingCallback;

    /**
     * Get a policy instance for a given class.
     *
     * @param  object|string  $class
     * @return mixed
     */
    public function getPolicyFor($class)
    {
        if ($this->guessClassNameUsingCallback && !is_object($class)) {
            $class = call_user_func($this->guessClassNameUsingCallback, $class);
        }

        return parent::getPolicyFor($class);
    }

    /**
     * Specify a callback to be used to guess policy models.
     *
     * @param  callable $callback
     * @return $this
     */
    public function guessClassNameUsing(callable $callback)
    {
        $this->guessClassNameUsingCallback = $callback;

        return $this;
    }
}

Let's review this file quick, first off the getPolicyFor function.

    /**
     * Get a policy instance for a given class.
     *
     * @param  object|string  $class
     * @return mixed
     */
    public function getPolicyFor($class)
    {
        if ($this->guessClassNameUsingCallback && !is_object($class)) {
            $class = call_user_func($this->guessClassNameUsingCallback, $class);
        }

        return parent::getPolicyFor($class);
    }

We are overriding the default getPolicyFor function that the Laravel Gate class uses to get the model for the policy. If we have set a callback, then use that to get the class and then pass the value to the parents function.

The guessClassNameUsing() is setting the internal callback in the same way as the guessPolicyName function does. This allows the functionality to be opt-in.

That's about it for custom classes :)

Extending the Laravel Gate class

We are now going to let Service Container in Laravel know that we are using a different class when resolving the Gate contract. This means, that if we instantiate the Gate class directly, it won't have the functionality we adding.

We will place the following in the AppServiceProvider class inside the register function

    $this->app->singleton(\Illuminate\Contracts\Auth\Access\Gate::class, function ($app) {
        return new \App\Extensions\CustomGate($app, function () use ($app) {
            return call_user_func($app['auth']->userResolver());
        });
    });

Now that we have done that we can move onto how we can map our classes.

Creating a class map.

I use a single config file to keep all the Model classes for Morph Mapping in relations, for that reason I will give that example of a way to map your classes in a central place with default names. This is done as I have a few models and an array inside the Service Provider is a bit of a pain.

Right, so for example you can structure a config file (config/models.php), or even a trait like the following:

<?php
// filename: config/models.php
return [
    /*
    |--------------------------------------------------------------------------
    | Morph Maps
    |--------------------------------------------------------------------------
    |
    | The config for the morph Map relations
    |
    */
    'map' => [
        'users'         => \App\Models\User::class,
        'applications'  => \App\Models\Application::class,
        'comments'      => \App\Models\Comment::class,
        // another 20 odd models
        'components'    => \App\Models\Forms\Component::class,
        'templates'     => \App\Models\Forms\Template::class,
    ],
];

Now you would have that named how you like and also your model classes will be different.

But the important bit is that you can now write config('models.map.users') and get \App\Models\User::class.

Now with that class map, it is a simple matter of giving the CustomGate class a callback and this list and we are set.

Linking the string to a Model class

Open up the AuthServiceProvider and go to the boot() function as for the reason of placing the auth related part of this exercise; it is the most logical.

    Gate::guessClassNameUsing(function($class) {
        return Arr::get(config('models.map'), $class, $class);
    });

Don't forget to add use Illuminate\Support\Facades\Gate; and use Illuminate\Support\Arr; to the top of the class :)

The callback for linking the class sting to an actual class is pretty simple

  1. Look in the model map array for $class
  2. If its found by Arr::get() return the result
  3. if not send back the original string and let the Parent of the Gate class handle it.

How to use this now.

Well a simple example is the following:

@can('create', 'users')
    // some element or so in the 
@endcan

Now the cool thing is you can still use App\Models\User::class and it will still get picked up.

Also if you pass the string value 'App\Models\Class', because it isn't found in the array map, it returns the search string instead of the default null.

I hope you enjoyed the article, if you have any questions you can reach me on Twitter at @iexistin3d