Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Fluent Policy Laravel Package

soyhuce/laravel-fluent-policy

Define Laravel authorization policies as fluent, reusable method chains instead of repetitive gate checks. soyhuce/laravel-fluent-policy keeps access rules readable and composable, integrating with Laravel's Gate so complex permissions stay inline, testable, and easy to maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

soyhuce/laravel-fluent-policy is a small, single-purpose package: it adds a FluentPolicy trait you use inside a normal Laravel policy class, and a tiny response builder that lets you express "allow/deny" rules as a readable chain instead of nested ifs. There is no config file to publish and no service provider wiring to do — that is the whole appeal.

First steps:

  1. composer require soyhuce/laravel-fluent-policy (1.8.0 requires Laravel 12; stay on 1.7.x if you are pinned to Laravel 11 or lower).
  2. Generate a policy as usual (php artisan make:policy PostPolicy --model=Post) and add use FluentPolicy; to the class.
  3. Read the README's single example and then the trait + builder source in src/ — it is a few hundred lines and you can read it in one sitting. That is faster than guessing semantics from the docs.
  4. Convert one non-trivial ability (something with 3+ conditions, e.g. update on a tenant-scoped model) and run your existing policy tests against it.

Do not convert your whole app/Policies directory on day one. The trait only changes how you write a policy method; Laravel's discovery (App\Policies\*Policy), Gate::policy(), @can, authorize(), and form requests are all unchanged, so a mixed codebase works fine and lets you migrate method by method.

Implementation Patterns

Condition chain instead of early returns. The core pattern is turning a branch tree into a single expression:

// before
public function update(User $user, Post $post): bool
{
    if ($user->isAdmin()) {
        return true;
    }

    return $post->author_id === $user->id && ! $post->is_locked;
}

// after
public function update(User $user, Post $post): Response
{
    return $this->allow()
        ->when($user->isAdmin())
        ->orWhen($post->author_id === $user->id && ! $post->is_locked);
}

Return Response (Laravel's Illuminate\Auth\Access\Response) rather than bool on methods where you want a deny reason — the message lands in the AuthorizationException and therefore in your 403 body when APP_DEBUG is on and in whatever handler you write.

denyAsNotFound() for existence-leaking resources. For multi-tenant or private-by-default models, use the not-found variant instead of a plain deny so $user->can(...) inside authorize()/Gate::authorize() produces a 404. This is the single biggest day-to-day win over hand-written Response::denyAsNotFound() scattered through policies.

Separate "who" from "what". Keep the condition helpers doing pure checks ($user->isAdmin(), $post->isPublished(), $user->owns($post)) and let the policy read as a sentence. That keeps the chain testable and greppable.

Use before() for the super-admin bypass. The trait does not replace before(); it still runs first and true there short-circuits everything, so a global admin override belongs there, not in every chain.

Compose with scopes, not queries. If a policy condition needs data ($user->teams()->whereKey($team)->exists()), load it in the controller or a DTO and pass a bool into the chain — otherwise you get N+1s inside @can loops in Blade.

Testing stays identical. Gate::forUser($user)->allows('update', $post) and $this->actingAs($user)->getJson(...)->assertForbidden()/assertNotFound() cover it; the fluent builder is an implementation detail, so port your existing policy tests verbatim before refactoring.

Gotchas and Tips

  • 1.8.0 is a maintenance release. The only substantive change is the CI matrix running against Laravel 12, plus dependabot bumps to GitHub Actions. No new API, no deprecations, no breaking changes — but it does confirm active Laravel 12 support, so this is the version to adopt on a Laravel 12 upgrade. Upgrading from 1.7.0 is a composer update with no code changes.
  • PHP constraint follows Laravel. Laravel 12 means PHP 8.2+; if your composer.json still allows 8.1, resolve that before bumping.
  • Method names on the builder have accreted across 1.x. If a chained call you saw in a blog post doesn't autocomplete, check the CHANGELOG for when it was introduced — the version in the wild and the version in the README are not always the same.
  • The chain must end in a resolved response. Returning the builder from inside a closure, storing it on a property, or dump()ing it mid-chain gives you a builder object, not a verdict; Gate will then evaluate it as truthy. Always return the final call directly.
  • Nullability for guests. Laravel will not invoke a policy for a guest when the Gate check has no user and the policy is type-hinted User; if you want guests to get a 403 with a message rather than a blanket deny, type-hint ?User and handle it as the first condition.
  • before() returning false in a global base policy silently kills every fluent chain. Keep it returning null for "no opinion".
  • Debugging denials: the message only survives if you return a Response; a method returning bool discards it. If your 403s are mysteriously generic, check the return type first.
  • Recursive checks. Calling $user->can(...) on the same ability from inside a condition is an infinite loop — assert preconditions instead of re-entering the gate.
  • Extension point: since it is just a trait, the practical pattern is a base App\Policies\Policy class that uses FluentPolicy and adds your own helpers (tenant checks, isOwner() sugar). Do not fork the package for that; the source is small enough to read but not worth vendoring.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource