Policy auto-discovery vs manual registration
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:
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
// 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'];
}
}