Laravel Policies and Gates Reference
A reference for Laravel policies and gates: authorizing what a user can do, beyond simple route middleware.
Laravel Reference
Gates vs policies
A gate is a simple closure-based authorization check, good for actions that aren't tied to a specific model (e.g. "can view the admin panel"). A policy groups all the authorization logic for one model — and is what most apps end up using once there's more than a rule or two.
Gates
// AppServiceProvider::boot()
Gate::define('view-admin-panel', function (User $user) {
return $user->role === 'admin';
});
if (Gate::allows('view-admin-panel')) {
// ...
}
Gate::authorize('view-admin-panel'); // throws 403 automatically if denied
Creating a policy
php artisan make:policy PostPolicy --model=Post
class PostPolicy
{
public function view(User $user, Post $post): bool
{
return $post->published || $user->id === $post->user_id;
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->role === 'admin';
}
}
Laravel auto-discovers a policy named {Model}Policy for a given model — no manual registration needed in modern Laravel versions, as long as it lives in the conventional app/Policies location.
Checking a policy
// In a controller
public function update(Request $request, Post $post)
{
$this->authorize('update', $post); // 403s automatically if denied
// ...
}
// Anywhere else
if ($user->can('update', $post)) {
// ...
}
if ($user->cannot('delete', $post)) {
abort(403);
}
In Blade
@can('update', $post)
<a href="/posts/{{ $post->id }}/edit">Edit</a>
@endcan
@cannot('delete', $post)
<p>You cannot delete this post.</p>
@endcannot
Authorizing in Form Requests
class UpdatePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update', $this->route('post'));
}
}
Because a Form Request's authorize() method runs before validation, a denied request fails fast with a 403 and never touches the validation rules. This is a good place to check ownership or role-based access without cluttering the controller method itself.
Resource controllers and authorizeResource
A policy's method names line up with the seven resource controller actions — viewAny, view, create, update, delete, plus restore and forceDelete for soft-deleted models. Rather than calling $this->authorize() in every method, a resource controller can authorize all of them in one line via its constructor:
class PostController extends Controller
{
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
// viewAny() is checked before index(), view() before show(),
// update() before update(), delete() before destroy(), and so on.
}
Note that viewAny and create don't receive a model instance — there's nothing to check ownership against yet — so those two methods only take the $user argument.
Gate::before and Gate::after
Both gates and policies can be short-circuited globally. A common use is granting a super-admin role blanket access without having to repeat that check in every single policy method:
// AppServiceProvider::boot()
Gate::before(function (User $user, string $ability) {
if ($user->isAdmin()) {
return true; // skips the specific gate/policy check entirely
}
});
Gate::after(function (User $user, string $ability, ?bool $result, mixed $arguments) {
// Runs after every check, even ones already resolved by before().
// Useful for auditing/logging authorization decisions.
});
Returning null (or nothing) from a before callback lets the normal gate or policy method decide as usual; returning true or false overrides it immediately.
Custom denial messages
By default a failed authorization check just throws a generic 403. To surface a more specific reason to the user, return a Response object from the policy method instead of a boolean:
use Illuminate\Auth\Access\Response;
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('You do not own this post.');
}
The custom message is available on the caught AuthorizationException and is what gets shown when $this->authorize() aborts the request.