Use Morph Map of names to resolve the Model for Laravel Gate Policies
Tags: laravel, php, tutorial, authentication
Published: 2020-07-06 21:44:36

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.

<!-- more -->

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
<?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.

```php
	/**
     * 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, <mark class="bg-code-yellow italic">it won't have the functionality we adding.</mark>

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

```php
	$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
<?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.

```php
	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: 

```php
@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](https://twitter.com/iexistin3d) or comment below.