Policies & Gates
Centralize authorization: who is allowed to do what, expressed as small, testable rules.
Authorization is not validation. Validation asks "is this data well-formed?"; authorization asks "is this user allowed to do this?" Laravel gives you two tools: Gates for simple, model-free checks and Policies for permissions grouped around a model.
Gates
A closure registered in a provider, keyed by an ability name. Good for coarse checks like view-admin-panel.
Gate::define('view-admin', fn ($user) => $user->is_admin);
if (Gate::allows('view-admin')) { ... }Policies
A class whose methods (view, update, delete) map to abilities on a model. Laravel auto-discovers PostPolicy for Post. You then check with $user->can('update', $post), the @can Blade directive, or $this->authorize('update', $post) in a controller.
The before() hook & responses
A policy's before() can grant super-admins everything up front. And returning a Response::deny('message') instead of false gives the user a reason, not just a bare 403.
Example
<?php
namespace App\Policies;
use App\Models\{Post, User};
use Illuminate\Auth\Access\Response;
class PostPolicy
{
// Runs before every ability: super-admins bypass all checks.
public function before(User $user, string $ability): ?bool
{
return $user->is_super_admin ? true : null; // null = defer to method
}
public function view(?User $user, Post $post): bool
{
// Guests (?User null) may view published posts.
return $post->published || $user?->id === $post->user_id;
}
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('You can only edit your own posts.');
}
public function delete(User $user, Post $post): Response
{
if ($post->published) {
return Response::deny('Unpublish before deleting.');
}
return $user->id === $post->user_id ? Response::allow() : Response::denyAsNotFound();
}
}
// --- Enforcing it ---
// In a controller: $this->authorize('update', $post);
// In Blade: @can('update', $post) ... @endcan
// On a route: ->middleware('can:update,post');When to use it
- A PostPolicy's update() method checks that the authenticated user is the author of the post before allowing the update, centralising the ownership rule in one class.
- An admin panel uses $this->authorize('viewAny', Post::class) in a controller to block non-admin users from seeing the post list without duplicating the check in every action.
- A Blade template uses @can('delete', $post) to hide the delete button from users who do not have the required policy permission, keeping the view security-aware.
More examples
Generate and define a policy
The --model flag pre-fills all seven resource method stubs (viewAny, view, create, update, delete, restore, forceDelete) with the Post model type-hinted.
php artisan make:policy PostPolicy --model=Post
# Creates app/Policies/PostPolicy.php
# Register it in app/Providers/AuthServiceProvider.phpPolicy method and registration
The policy methods receive the authenticated user and model instance; the AuthServiceProvider maps the model to the policy class.
// app/Policies/PostPolicy.php
class PostPolicy
{
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->isAdmin();
}
}
// app/Providers/AuthServiceProvider.php
protected $policies = [
Post::class => PostPolicy::class,
];Authorise in a controller and Blade view
authorize() in the controller throws a 403 if the policy fails; @can in Blade conditionally renders UI elements based on the same policy.
// In a controller
public function update(UpdatePostRequest $request, Post $post): RedirectResponse
{
$this->authorize('update', $post); // 403 if policy returns false
$post->update($request->validated());
return redirect()->route('posts.show', $post);
}
{{-- In a Blade template --}}
@can('delete', $post)
<form method="POST" action="{{ route('posts.destroy', $post) }}">
@csrf @method('DELETE')
<button>Delete</button>
</form>
@endcan
Discussion