laravel-policies

Installation
SKILL.md

Laravel Policies

A policy answers one question: may this user do this to this kind of record? It answers from the user's permissions and, where it matters, whether they own the record. Whether the record is in a state that allows the operation is a different question. An order can't be cancelled once it has shipped, whoever asks; the action's guard enforces that and throws a domain exception that says why. Routes call policies, and a policy never does anything but answer.

Rules

  • Permission and ownership only. No state checks ($order->isShipped(), canTransitionTo()): they belong in the action's guard (laravel-actions). A state rule in a policy fails with a bare "This action is unauthorized." and duplicates the guard the action needs anyway.
  • Ask the project's permission system, by enum. $user->can(Permission::CancelOrders), never a string. Laravel's Gate accepts enum abilities, and a permission system that hooks the Gate (spatie/laravel-permission does) answers them.
  • Permissions, never role names. Roles are data an admin re-grants. hasRole('manager') in a policy hard-codes a grant that belongs in the role's permissions.
  • Ownership is a model method. $order->notOwnedBy($user), never $order->user_id !== $user->id in the policy. The model knows how it's owned, and every policy reads the same.
  • Early returns, ending on the permission. A check with more than one condition is a run of if (…) { return false; }, closing on return $user->can(...); not one long && chain.
  • Return bool. A denial needs no message: it's a 403. A rule that needs explaining is a state rule, and its exception explains it.
  • Abilities are named after what they protect. Resource abilities keep Laravel's names (viewAny, view, create, update, delete). Any other ability is the verb of the action it guards: cancel for CancelOrderAction.
  • viewAny doesn't filter. It says whether the user may list orders at all. Which orders they see is a constraint on the listing's query (laravel-query-builders).
  • Permission cases are {Verb}{Subject}; values are '{verb} {subject}', subject kebab-cased: CancelOrders = 'cancel orders', ViewAllOrders = 'view all-orders'. The space is the one departure from laravel-enums' kebab-case values. A value is a stored permission name that roles are granted by, so it never changes once deployed.
  • Wire the policy with #[UsePolicy] on the model (Laravel 12.18+; laravel-models owns the attribute). Below 12.18, auto-discovery finds App\Policies\OrderPolicy by name; a policy outside that convention is registered with Gate::policy() in a provider's boot().
  • Routes call policies. Every route on a surface with a permission model carries ->can(), and nothing calls authorize(). laravel-routing owns the rule.

Example

Installs
59
GitHub Stars
15
First Seen
Mar 27, 2026