While gates handle general authorization, policies organize model-specific permissions. But how does Laravel know which policy to use when you call $this->authorize('update', $post)? The policy resolution system is Laravel's intelligent mechanism for automatically discovering and executing the right authorization logic. Let's explore this sophisticated discovery process.
What Are Policies?
Policies are classes that organize authorization logic around a specific model. Instead of scattering authorization checks throughout your application, policies centralize all permissions for a model:
namespace App\Policies;
use App\Models\User;
use App\Models\Post;
class PostPolicy
{
public function view(User $user, Post $post)
{
return $post->published || $user->id === $post->user_id;
}
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
All authorization logic for the Post model lives in one organized class.
Policy Registration
Policies can be registered explicitly or discovered automatically.
Explicit Registration
// app/Providers/AuthServiceProvider.php
use App\Models\Post;
use App\Policies\PostPolicy;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Post::class => PostPolicy::class,
];
public function boot()
{
$this->registerPolicies();
}
}
This explicitly maps the Post model to PostPolicy.
Automatic Discovery
Laravel 12 automatically discovers policies without registration:
// Laravel looks for policies in App\Policies
// Naming convention: {ModelName}Policy
App\Models\Post → App\Policies\PostPolicy
App\Models\Comment → App\Policies\CommentPolicy
App\Models\User → App\Policies\UserPolicy
If you follow the naming convention, registration is optional.
The Policy Resolution Flow
When you call authorization methods, Laravel follows a precise resolution process:
Step 1: Authorization Trigger
// In a controller
$this->authorize('update', $post);
// Behind the scenes
Gate::authorize('update', $post);
The authorize() helper triggers the Gate system.
Step 2: Model Class Detection
// Behind the scenes
$modelClass = get_class($post);
// Result: App\Models\Post
Laravel extracts the model's fully qualified class name.
Step 3: Policy Lookup
Laravel searches for a policy in this order:
3a. Check Explicit Registration
// Behind the scenes
$policy = $this->policies[$modelClass] ?? null;
if ($policy) {
return app($policy); // Found in $policies array
}
3b. Check Automatic Discovery
// Behind the scenes
$policyClass = $this->guessPolicyName($modelClass);
// Result: App\Policies\PostPolicy
if (class_exists($policyClass)) {
return app($policyClass); // Found via naming convention
}
The guessPolicyName() method transforms model names:
// Behind the scenes
protected function guessPolicyName($modelClass)
{
$namespace = 'App\Policies\\';
$baseName = class_basename($modelClass); // "Post"
return $namespace . $baseName . 'Policy'; // "App\Policies\PostPolicy"
}
Step 4: Method Resolution
Once the policy is found, Laravel looks for the ability method:
// For: $this->authorize('update', $post)
// Laravel looks for:
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
The ability name ('update') maps to the method name (update()).
Step 5: Method Execution
// Behind the scenes
$user = Auth::user();
$result = $policy->update($user, $post);
if ($result === true) {
return; // Authorized
}
throw new AuthorizationException('This action is unauthorized.');
Laravel calls the policy method and evaluates the result.
Policy Method Signatures
Policy methods follow specific signatures:
With Model Instance
public function update(User $user, Post $post)
{
// Check if user can update THIS specific post
return $user->id === $post->user_id;
}
Receives the authenticated user and the model instance.
Without Model Instance
public function create(User $user)
{
// Check if user can create ANY post
return $user->hasRole('author');
}
For abilities that don't require a specific model (like create).
Guest User Support
public function view(?User $user, Post $post)
{
// Allow guests to view published posts
if (!$user) {
return $post->published;
}
return $post->published || $user->id === $post->user_id;
}
The nullable ?User parameter allows guest access.
The before() Method
Policies support a special before() method that runs before any specific ability check:
class PostPolicy
{
public function before(User $user, string $ability)
{
// Admins can do everything
if ($user->isAdmin()) {
return true;
}
// Return null to continue to specific methods
}
}
If before() returns non-null, the specific ability method is skipped:
// Authorization flow with before()
$beforeResult = $policy->before($user, 'update');
if ($beforeResult !== null) {
return $beforeResult; // Skip update() method
}
// Continue to update() method
return $policy->update($user, $post);
This is perfect for super-admin bypass logic.
Using Policies
In Controllers
public function update(Request $request, Post $post)
{
// Method 1: authorize() helper
$this->authorize('update', $post);
// Method 2: Gate facade
Gate::authorize('update', $post);
// Method 3: Check without exception
if (Gate::denies('update', $post)) {
abort(403);
}
$post->update($request->validated());
}
In Blade Templates
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
@cannot('delete', $post)
<p>You cannot delete this post</p>
@endcannot
The @can directive automatically uses the policy.
In Routes
// Middleware protection
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::delete('/posts/{post}', [PostController::class, 'destroy'])
->can('delete', 'post');
Via User Model
if ($user->can('update', $post)) {
// User can update
}
if ($user->cannot('delete', $post)) {
// User cannot delete
}
Policy Discovery Configuration
Customize how Laravel discovers policies:
// In AuthServiceProvider
use Illuminate\Support\Facades\Gate;
public function boot()
{
// Custom policy namespace
Gate::guessPolicyNamesUsing(function ($modelClass) {
return 'App\\Authorization\\' . class_basename($modelClass) . 'Policy';
});
}
This changes the default namespace from App\Policies to App\Authorization.
Authorizing Multiple Abilities
Check multiple abilities at once:
// Any of these abilities
if (Gate::any(['update', 'delete'], $post)) {
// User can update OR delete
}
// All of these abilities
if (Gate::check(['update', 'publish'], $post)) {
// User can update AND publish
}
In Blade:
@canany(['update', 'delete'], $post)
<div class="post-actions">
<!-- User can perform at least one action -->
</div>
@endcanany
Response Objects in Policies
Policies can return Response objects for detailed error messages:
use Illuminate\Auth\Access\Response;
public function update(User $user, Post $post)
{
if ($user->id === $post->user_id) {
return Response::allow();
}
if ($post->locked) {
return Response::deny('This post is locked for editing.');
}
return Response::deny('You do not own this post.');
}
The custom messages appear in authorization exceptions:
$this->authorize('update', $post);
// Throws: AuthorizationException: "This post is locked for editing."
Policy Methods Without Authorization
Sometimes you want policy methods that aren't authorization checks:
class PostPolicy
{
public function update(User $user, Post $post)
{
return $this->isOwner($user, $post);
}
// Helper method - not an authorization ability
protected function isOwner(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
Only public methods are treated as authorization abilities. protected and private methods are helpers.
Resource Controllers and Policies
Laravel maps resource controller methods to policy methods automatically:
// Controller method → Policy method mapping
index() → viewAny()
show() → view()
create() → create()
store() → create()
edit() → update()
update() → update()
destroy() → delete()
Example policy for resource controller:
class PostPolicy
{
public function viewAny(User $user)
{
return true; // Anyone can view list
}
public function view(User $user, Post $post)
{
return $post->published || $user->id === $post->user_id;
}
public function create(User $user)
{
return $user->hasRole('author');
}
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
Then use resource route with authorization:
Route::resource('posts', PostController::class)
->middleware('auth');
Authorization happens automatically via the authorize() calls in your controller.
Inspecting Policy Results
Get detailed information about authorization:
$response = Gate::inspect('update', $post);
if ($response->allowed()) {
echo "Authorized";
echo $response->message(); // Optional success message
} else {
echo "Not authorized";
echo $response->message(); // Error message
echo $response->code(); // HTTP status code
}
Performance Considerations
Policy resolution adds minimal overhead:
// First authorization check:
// 1. Policy discovery (~1ms)
// 2. Policy instantiation (~1ms)
// 3. Method execution (varies)
// Total: 2-10ms
// Subsequent checks in same request:
// Policy is cached in service container
// Total: <1ms
Laravel caches resolved policies per request:
// First call - discovers and instantiates policy
Gate::allows('update', $post);
// Second call - uses cached policy instance
Gate::allows('delete', $post);
Common Patterns
Owner-Based Authorization
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post)
{
return $user->id === $post->user_id;
}
Role-Based Authorization
public function publish(User $user, Post $post)
{
return $user->hasRole('editor') || $user->hasRole('admin');
}
Combined Conditions
public function update(User $user, Post $post)
{
return $user->id === $post->user_id
&& !$post->locked
&& !$post->published;
}
Debugging Policies
Check If Policy Exists
$policyClass = Gate::getPolicyFor(Post::class);
if ($policyClass) {
echo "Policy found: " . get_class($policyClass);
} else {
echo "No policy registered";
}
List All Policies
$policies = Gate::policies();
foreach ($policies as $model => $policy) {
echo "$model → $policy\n";
}
Best Practices
1. Follow Naming Conventions
// Good - automatically discovered
App\Models\Post → App\Policies\PostPolicy
// Bad - requires manual registration
App\Models\Post → App\Policies\PostAuthorizationHandler
2. Use before() for Admin Bypass
public function before(User $user, string $ability)
{
if ($user->isAdmin()) {
return true; // Admins bypass all checks
}
}
3. Return Response Objects
// Provides better error messages
return Response::deny('Post is locked');
// Instead of just
return false;
4. Keep Logic in Policies
// Don't duplicate logic
if ($user->id === $post->user_id) { ... } // In multiple places
// Centralize in policy
public function update(User $user, Post $post) { ... }
Conclusion
Laravel's policy resolution system elegantly handles model-specific authorization through automatic discovery, method mapping, and intelligent caching. By understanding how Laravel finds policies, resolves methods, and executes authorization checks, you can build organized, maintainable authorization systems.
Policies transform scattered authorization logic into clean, testable classes that make your application's permission structure immediately clear.
Need help implementing complex authorization systems in Laravel? At NeedLaravelSite, we specialize in Laravel security and application migrations from version 7 to 12. From role-based access control to multi-tenant policies, we build secure, scalable authorization systems.