0

Policy auto-discovery vs manual registration

Intermediate5 min read·lv-17-004

Concept

Policy auto-discovery (Laravel 10+) automatically registers policies by convention: App\Policies\PostPolicy for App\Models\Post. No AuthServiceProvider registration needed if you follow the convention.

How auto-discovery works: When $user->can('update', $post) is called, Laravel's GateFactory looks for a policy class by convention — it searches for {ModelNamespace}Policies\{ModelName}Policy or App\Policies\{ModelName}Policy. If found, it uses it automatically.

Manual registration (override or for non-standard paths): In AuthServiceProvider::$policies array, or Gate::policy(Model::class, Policy::class) in boot:

php
protected $policies = [
    Post::class => PostPolicy::class,
    App\Models\Legacy\OldPost::class => App\Policies\PostPolicy::class,
];

Customizing discovery: Gate::guessPolicyNamesUsing(callable $callback) — override the discovery logic for non-standard namespacing or multiple model directories.

can middleware: Apply policy checks in route definitions: ->middleware('can:update,post'). The post refers to the route parameter name bound via route model binding.

Authorization in form requests: Override authorize() method in Form Requests to check policies: return $this->user()->can('update', $this->post). Returns true (allow) or false (403).

Code Example

php
<?php
// Auto-discovery (default in Laravel 10+)
// App\Models\Post → App\Policies\PostPolicy  (discovered automatically)
// App\Models\User → App\Policies\UserPolicy  (discovered automatically)

// Manual registration — in app/Providers/AuthServiceProvider.php
namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        // Override auto-discovery for this model
        \App\Models\Post::class => \App\Policies\BlogPostPolicy::class,  // non-standard name
        // Legacy model in non-standard namespace
        \App\Legacy\Order::class => \App\Policies\OrderPolicy::class,
    ];

    public function boot(): void
    {
        $this->registerPolicies();
    }
}

// Custom discovery logic — for non-standard namespaces
Gate::guessPolicyNamesUsing(function(string $modelClass): string {
    // Map from 'App\Models\Sub\Post' to 'App\Policies\Sub\PostPolicy'
    $parts = explode('\\', $modelClass);
    $model = array_pop($parts);
    $namespace = implode('\\', $parts);
    return str_replace('Models', 'Policies', $namespace) . '\\' . $model . 'Policy';
});

// Route middleware — check policy before route is handled
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])
     ->middleware(['auth', 'can:update,post']); // 'post' = route parameter name

Route::delete('/posts/{post}', [PostController::class, 'destroy'])
     ->middleware(['auth', 'can:delete,post']);

// Form Request with policy authorization
class UpdatePostRequest extends \Illuminate\Foundation\Http\FormRequest
{
    public function authorize(): bool
    {
        $post = $this->route('post'); // get the route-bound model
        return $this->user()->can('update', $post);
    }

    public function rules(): array
    {
        return ['title' => 'required|string|max:255', 'body' => 'required|string'];
    }
}