0

Policies — authorization organized around models

Intermediate5 min read·lv-17-002
interview

Concept

Policies organize authorization logic into classes scoped to a specific Eloquent model. Each method in a policy corresponds to an ability (view, create, update, delete, restore, forceDelete) or a custom action.

Creating policies: php artisan make:policy PostPolicy --model=Post. Generates app/Policies/PostPolicy.php with stubs for all CRUD abilities.

Policy structure: The first argument is always the authenticated User. The second is the model instance (except for create which receives none, since the model doesn't exist yet).

Policy registration: Two approaches:

  1. Auto-discovery (Laravel 10+): Policies in app/Policies/ following the ModelNamePolicy convention are auto-discovered.
  2. Manual: Register in AuthServiceProvider::$policies: [Post::class => PostPolicy::class].

Using policies: Gate::authorize('update', $post), $this->authorize('update', $post) (in controllers extending Controller), $user->can('update', $post).

Policy for action without a model: $this->authorize('create', Post::class) — passes the class name instead of an instance.

@can directive with models: @can('update', $post).

Guest handling: By default, if the user is not logged in, all policy methods return false automatically. To allow guests to pass specific methods, type-hint the first argument as ?User.

Code Example

php
<?php
namespace App\Policies;

use App\Models\User;
use App\Models\Post;

class PostPolicy
{
    // view — can this user view this post?
    public function view(User $user, Post $post): bool
    {
        return $post->is_published || $user->id === $post->user_id;
    }

    // create — no model instance (post doesn't exist yet)
    public function create(User $user): bool
    {
        return $user->email_verified_at !== null;
    }

    // update
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    // delete
    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id && !$post->is_published;
    }

    // restore (soft deletes)
    public function restore(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    // Custom action
    public function publish(User $user, Post $post): bool
    {
        return $user->id === $post->user_id && $post->isDraft();
    }

    // Guest access — allow guests to view published posts
    public function viewPublished(?User $user, Post $post): bool
    {
        return $post->is_published; // user may be null (guest)
    }
}

// Using in controllers
class PostController extends Controller
{
    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post); // throws AuthorizationException → 403
        // ...
    }

    public function store(Request $request)
    {
        $this->authorize('create', Post::class); // class name for model-less
        // ...
    }

    public function publish(Post $post)
    {
        $this->authorize('publish', $post); // custom action
        // ...
    }
}

// Route model binding + policy — common pattern
Route::put('/posts/{post}', [PostController::class, 'update'])
     ->middleware(['auth', 'can:update,post']); // policy called via can middleware