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
- Look in the model map array for
$class
- If its found by
Arr::get()
return the result - 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 or comment below.
Top comments (0)