skip to content

How does Laravel find the policy for an Eloquent model, and which wins among Gate::policy, the #[UsePolicy] attribute and naming-convention discovery?

level: middleimportance: should knowfreq 40%

answer

  1. App\Models\Article to App\Policies\ArticlePolicy
  2. Models/Policies checked before Policies
  3. explicit registration first
  4. #[UsePolicy(ArticlePolicy::class)] on the model
  5. Gate::guessPolicyNamesUsing()

basics

~10 s

Laravel checks policies registered with Gate::policy first, then a #[UsePolicy] attribute on the model, then convention: ModelNamePolicy in a Policies directory at or above the model's, e.g. App\Policies\ArticlePolicy. Gate::guessPolicyNamesUsing() replaces the convention.

solid answer

~30 s

When a check receives an `Article` instance or `Article::class`, the Gate's `getPolicyFor()` tries, in order: a mapping registered with `Gate::policy(Article::class, ArticlePolicy::class)`; the model's `#[UsePolicy(ArticlePolicy::class)]` attribute (from `Illuminate\Database\Eloquent\Attributes`); convention-based discovery, which for `App\Models\Article` looks for `App\Models\Policies\ArticlePolicy` and then `App\Policies\ArticlePolicy`; a registered policy for a parent class; and finally `#[UsePolicy]` on a parent class. `php artisan make:policy ArticlePolicy --model=Article` writes `app/Policies/ArticlePolicy.php`, so the convention finds it with no registration. Use `Gate::guessPolicyNamesUsing()` to replace the naming rule for an unconventional layout, and `Gate::policy()` or the attribute when one model's policy lives elsewhere. The resolved policy comes from the container, so constructor injection works.

code

php · 13 lines
php
<?php

namespace App\Editorial\Models;

use App\Editorial\Policies\IssuePolicy;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;

#[UsePolicy(IssuePolicy::class)]
class Issue extends Model
{
    //
}

go deeper

for a junior

Remember the convention: App\Models\Article pairs with App\Policies\ArticlePolicy, generated by make:policy with --model.

for a middle

Explain the lookup order — Gate::policy, #[UsePolicy], convention — and why the first argument must be a model or class name.

for a senior

Choose between conventions, the attribute and guessPolicyNamesUsing for a modular codebase, and debug a policy that is never reached.

for a principal

Set a layout convention for policies across modules so discovery stays predictable as teams add domains.

## Why discovery matters A policy is only useful if the Gate can map a model to it. When you call `Gate::authorize('update', $article)` or `Gate::authorize('create', Article::class)`, the Gate takes the first argument, gets its class name, and asks `getPolicyFor()` which policy handles it. If none is found, the Gate looks for a plain gate named after the ability, and otherwise denies. ## The lookup order (Laravel 13 source) 1. **Explicit registration** — `Gate::policy(Article::class, ArticlePolicy::class)` in `AppServiceProvider::boot()`. 2. **The model's own attribute** — `#[UsePolicy(ArticlePolicy::class)]` from `Illuminate\Database\Eloquent\Attributes\UsePolicy`. 3. **Convention-based discovery** — a class named `<Model>Policy` in a `Policies` directory at or above the model's directory. For `App\Models\Article`, the candidates are checked in this order: - `App\Models\Policies\ArticlePolicy` - `App\Policies\ArticlePolicy` 4. **A registered parent** — if `Article` extends a class that has a `Gate::policy` mapping. 5. **An attribute on a parent class** — `#[UsePolicy]` found by walking up the model's parents. The first match wins, and the policy is built with `$container->make()`. ## Generating a policy the convention will find ```bash php artisan make:policy ArticlePolicy --model=Article ``` - Writes `app/Policies/ArticlePolicy.php`, the location discovery checks. - With `--model`, the stub contains `viewAny`, `view`, `create`, `update`, `delete`, `restore` and `forceDelete`, each typed and each returning **`false`**. A freshly generated policy therefore denies everything until you fill it in. - Without `--model` the class is empty. ## When to step off the convention | Situation | Tool | |---|---| | Standard `app/Models` and `app/Policies` layout | nothing — discovery finds it | | One model whose policy lives in a module folder | `#[UsePolicy(...)]` on the model, or `Gate::policy()` | | A modular app with its own naming scheme everywhere | `Gate::guessPolicyNamesUsing(fn (string $model) => ...)` | | A policy for a non-Eloquent class | `Gate::policy()` | `#[UsePolicy]` keeps the link next to the model, which is easy to find when reading the model; `Gate::policy()` keeps all mappings in one provider and overrides the attribute when both exist. ## Diagnosing "my policy is never called" 1. Is the policy in a directory discovery checks, with the exact `<Model>Policy` name? 2. Did you pass the **model** (or its class name) as the first argument? `Gate::authorize('update')` with no argument can only reach a gate. 3. Does the policy have a method named after the ability? Abilities with dashes are camel-cased (`publish-now` → `publishNow`). 4. Is a global `Gate::before` returning a non-null value first? 5. Is a stale mapping in `Gate::policy()` pointing somewhere else? ## A publishing-platform layout - `app/Models/Article.php -> app/Policies/ArticlePolicy.php (discovered)` - `app/Models/Review.php -> app/Policies/ReviewPolicy.php (discovered)` - `app/Editorial/Models/Issue.php -> #[UsePolicy(IssuePolicy::class)] (explicit)` ## Replacing the naming rule For a modular application where each module keeps its own `Policies` folder next to `Models`, a single callback can replace convention discovery: ```php Gate::guessPolicyNamesUsing(function (string $modelClass) { return str_replace('\\Models\\', '\\Policies\\', $modelClass).'Policy'; }); ``` The callback's result replaces the built-in guesses entirely, so it must cover every model that relies on discovery. Explicit `Gate::policy()` mappings and `#[UsePolicy]` attributes are still checked before it. ## Container resolution Because policies are built with the container, a policy can depend on services: - a `SectionAssignments` repository to ask which sections an editor owns; - a feature-flag service to gate a new review workflow. Keep those dependencies cheap: a policy method may run many times per request, for example once per `@can` in a loop over articles.

  • An Article model has #[UsePolicy(LegacyArticlePolicy::class)] and AppServiceProvider calls Gate::policy(Article::class, ArticlePolicy::class). Which policy runs?
    `ArticlePolicy`. `getPolicyFor()` checks the policies registered with `Gate::policy()` before reading the model's attribute, and conventions come after both. Remove one of the two declarations so readers are not misled.
  • What does a policy generated with make:policy --model return before you edit it?
    Every generated method (`viewAny`, `view`, `create`, `update`, `delete`, `restore`, `forceDelete`) returns `false`, so every check through it is denied. That is a safe default, but it also means wiring the policy into routes before writing the rules locks everyone out.

saying these in an interview costs you the question

  • Every policy must be registered in a provider before it works
  • The #[UsePolicy] attribute overrides Gate::policy registrations
  • Discovery looks for the policy next to the controller
  • A freshly generated --model policy allows everything
  • Passing only the ability name is enough to reach a policy