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 Policy Soft Cache Laravel Package

innoge/laravel-policy-soft-cache

Laravel package that adds a soft cache layer for authorization policies, storing policy check results to avoid repeated Gate evaluations and speed up permission checks. It integrates with Laravel's authorization system to reduce policy overhead while keeping your existing policy code unchanged.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (1.6.0)

composer require innoge/laravel-policy-soft-cache

Release 1.6.0 is a compatibility release: it adds Laravel 13 support and contains nothing else (single PR, no changelog entries for behaviour changes). There are no new features, deprecations, or breaking changes versus 1.5.0, so upgrading is a composer update away — the only thing that can bite you is the transitive PHP/Laravel constraint bump that comes with Laravel 13, not this package's own code.

First three things to look at

  1. The published config — php artisan vendor:publish and read the package's config file before writing any code. The knobs that matter day-to-day are whether caching is globally enabled, which cache store it uses, and the TTL. Confirm the actual publish tag from the service provider if --tag names differ.
  2. The service provider — it tells you whether the package hooks into the Gate (Gate::before/Gate::after) or expects you to opt in per policy. That distinction decides your whole integration approach.
  3. The trait/attribute or wrapper class it exposes — this is what you'll actually be typing in your policies.

First use case

Pick the policy ability that is measurably expensive — typically view, update, or viewAny on a model whose authorization requires a join across team/project membership tables. Leave the trivial slug-comparison policies alone; caching them adds a cache round-trip for nothing.

Verify it works in five minutes

DB::listen(fn ($q) => dump($q->sql));

Gate::forUser($user)->allows('update', $post);
Gate::forUser($user)->allows('update', $post); // second call should show no new queries

If you see the query twice, the policy isn't actually being wrapped — check the trait is applied and the config is enabled for that guard.

Implementation Patterns

Opt-in per policy, not globally. The safest rollout is to enable soft caching on the handful of policies that need it rather than flipping a global switch. A single stale decision on an account-deletion or billing policy is far more expensive than the queries you saved.

Key design is the whole game. Whatever the package does by default, sanity-check that the cache key includes: ability name, policy/model class, model primary key, the authenticated user's key, and the guard name. Omitting the guard is the classic bug in multi-guard apps (admin vs. web users sharing a cache entry). Also include a version/epoch token if you ever need a global flush on cache stores that lack tag support.

Pair it with explicit invalidation. "Soft" caching means you accept a window of staleness, but you should still bust the entry on the events that matter:

static::saved(fn ($model) => /* forget cached decisions for $model */);
static::deleted(fn ($model) => /* same */);

Critically, also bust when team/project membership or role assignments change — not just when the model itself is saved. A user removed from a team should lose access immediately, not at TTL expiry. This is the number one source of "why can this person still edit this?" tickets.

Workflow for a new expensive ability

  1. Write the policy normally, get the tests green.
  2. Measure: confirm the ability really is query-heavy before caching.
  3. Wrap it, keep TTL short (30–300s is a reasonable start).
  4. Add an invalidation observer for the model and any pivot/role models involved.
  5. Add tests that assert both the hit path and the invalidation path.

Escape hatch for critical paths. Keep a way to bypass the cache — a direct, uncached Gate check — and use it in jobs, webhooks, and any flow where a stale true has financial or security consequences. Treat cached authorization as a UI-optimisation layer, not the final enforcement point.

Queue/console contexts. Authorization inside queued jobs runs without an authenticated user and often with a different guard. Either skip caching there or make sure the key includes the acting user explicitly, otherwise a job may read a decision cached for a real request user.

Testing. Disable the cache in your test suite via config (or run the array store with a fresh instance per test) so tests read as plain authorization assertions. Add a small number of dedicated tests that specifically verify caching behaviour with Cache::spy() or DB::listen().

Gotchas and Tips

  • Null vs. false. Policies can return false, true, or a Response object with a message/status. Caching a Response and replaying it loses nuance if any code reads deniedMessage(). Prefer caching the boolean decision and letting the uncached path build the message when you need to display it.
  • Gate::before bypasses. Super-admin short-circuits in before() run ahead of the policy. Make sure you understand whether they're cached or always evaluated — an admin whose privileges were revoked should not be reading a cached true.
  • Tag support varies by store. redis/memcached support tags; file and database do not. If the package's invalidation relies on tags, a config change to the file driver in production silently degrades busting into a no-op. Pin the store explicitly and test invalidation on the store you actually deploy.
  • config:cache and Octane. Run php artisan config:cache after changing the package config, and restart Octane workers — long-lived workers will otherwise keep the old resolved config and any in-process state.
  • Stale cache after deploy. If you change policy logic, the old decisions linger until TTL expiry. Either deploy with a cache version bump in the key, or accept the TTL window — decide deliberately.
  • Debugging misses. Cache::spy() for assertions in tests, and a temporary DB::listen or query log in a staging environment for production-shaped debugging. If you can't see hit/miss, add a tiny log line around the wrapper rather than guessing.
  • Laravel 13 upgrade specifics. 1.6.0 itself is additive, but Laravel 13 raises the framework/PHP floor. Before bumping in a real app: check your PHP version, re-run the full test suite, and re-verify the cache store integration — framework major upgrades occasionally change cache/Gate internals even when this package's diff is one line.
  • Extension points. The config file and the package's gate hook are the supported seams. If you need custom key derivation or a different invalidation strategy, subclass/extend the package's wrapper rather than forking — the surface is small enough that a fork becomes a maintenance burden at the next Laravel major.
  • When not to use it. Cheap policies, policies with side effects, and one-off authorization checks in Artisan commands. The overhead of a cache round-trip plus the staleness risk rarely pays for itself on a policy that's a single indexed query.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels