I have to update a Laravel 8/9 application and I have an eli5 question about how implicitly bound middleware work.
I have a logged in route to update an entity as follows:
Auth::routes(['verify' => true]);
Route::group(['middleware' => ['auth', 'verified']], function () {
Route::group(['middleware' => ['a.b.c', 'can:update,entity']], function () {
Route::put('/admin/entity/{entity}', 'App\Http\Controllers\EntityController@updateEntity')->name('entity.updateEntity');
});
});
The auth
, verified
and a.b.c
middlewares I understand, I think:
auth
: check the user is logged in (check the auth()->user() return value?). It is a built-in middleware.verified
: check the user email has been verified (check the email_verified_at column in the DB?). It is a built-in middleware.a.b.c
: check the entity exists and can still be modified (custom middleware). It has been written for our application and I checked the code out.But the can:update,entity
middleware is implicitly bound, it checks the user can update the entity as I understand, but I cannot understand how the magic happens.
I checked the \Illuminate\Auth\Middleware\Authorize source code but it still does not click:
<?php
namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Database\Eloquent\Model;
class Authorize
{
/**
* The gate instance.
*
* @var \Illuminate\Contracts\Auth\Access\Gate
*/
protected $gate;
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Access\Gate $gate
* @return void
*/
public function __construct(Gate $gate)
{
$this->gate = $gate;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $ability
* @param array|null ...$models
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function handle($request, Closure $next, $ability, ...$models)
{
$this->gate->authorize($ability, $this->getGateArguments($request, $models));
return $next($request);
}
/**
* Get the arguments parameter for the gate.
*
* @param \Illuminate\Http\Request $request
* @param array|null $models
* @return \Illuminate\Database\Eloquent\Model|array|string
*/
protected function getGateArguments($request, $models)
{
if (is_null($models)) {
return [];
}
return collect($models)->map(function ($model) use ($request) {
return $model instanceof Model ? $model : $this->getModel($request, $model);
})->all();
}
/**
* Get the model to authorize.
*
* @param \Illuminate\Http\Request $request
* @param string $model
* @return \Illuminate\Database\Eloquent\Model|string
*/
protected function getModel($request, $model)
{
if ($this->isClassName($model)) {
return trim($model);
} else {
return $request->route($model, null) ??
((preg_match("/^['\"](.*)['\"]$/", trim($model), $matches)) ? $matches[1] : null);
}
}
/**
* Checks if the given string looks like a fully qualified class name.
*
* @param string $value
* @return bool
*/
protected function isClassName($value)
{
return str_contains($value, '\\');
}
}
Like what are the $gate
, $ability
?
Can anyone explain to me how it happens under the hood like I'm 5 please?
As pointed out by @Jaquarh, Authorization is usually implemented via Policies, that group authorization logic around a particular model or resource, and that use Gates for single validation objectives.
So how does that translate to my application?
App\Providers\AuthServiceProvider.php
in the policies
property:<?php
namespace App\Providers;
use App\Models\UserEntity;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Carbon\Carbon;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
'App\Models\Entity' => 'App\Policies\EntityPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
}
}
EntityPolicy
policy is defined in App\Policies\EntityPolicy.php
and defines what actions can be authorized on Entities for the current User: create, edit and update.
※ Note that all 3 validations are delegated to 2 Gates: can-create-entity
and user-has-entity
.<?php
namespace App\Policies;
use App\Models\Entity;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Support\Facades\Gate;
class EntityPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can create entities.
*
* @param \App\Models\User $user
* @return bool
*/
public function create(User $user)
{
return Gate::allows('can-create-entity');
}
/**
* Determine whether the user can edit the entity.
*
* @param \App\Models\User $user
* @param \App\Models\Entity $entity
* @return mixed
*/
public function edit(User $user, Entity $entity)
{
return Gate::allows('user-has-entity', $entity);
}
/**
* Determine whether the user can update the entity.
*
* @param \App\Models\User $user
* @param \App\Models\Entity $entity
* @return mixed
*/
public function update(User $user, Entity $entity)
{
return Gate::allows('user-has-entity', $entity);
}
}
App\Providers\AuthServiceProvider
class using the Gate
facade like follows:...
public function boot()
{
$this->registerPolicies();
Gate::define('user-has-entity', function($user, $entity) {
$userEntity = UserEntity::where('user_id', $user->id)
->where('entity_id', $entity->id)
->first();
return boolval($userEntity);
});
Gate::define('can-create-entity', function($user) {
$max = config('entities.MAX_AVAILABLE_ENTITIES');
$now = Carbon::now();
$current = $user
->entities()
->where('end', '>=', $now)
->count();
return $current < $max;
});
}
...
I had read the Laravel documentation page some time ago, but it did not make sense immediately for me, now I think I finally get it and I hope it can help someone else too.