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

Cache Query Laravel Package

laragear/cache-query

Cache your Laravel Eloquent queries and get fast, consistent results without hitting the database on every request. It hooks into the query builder, so you can cache models, counts, and relationships with a simple chainable call.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require laragear/cache-query
php artisan vendor:publish --provider="Laragear\CacheQuery\CacheQueryServiceProvider"

The package auto-registers via Laravel discovery; publishing gives you config/cache-query.php with the three knobs you actually care about: the cache store used for query results, the key prefix, and the default TTL.

Where to look first

  1. The config file — decide up front whether query caching shares the default cache store or gets its own (recommended, see below).
  2. The README's "Usage" section — the API surface is intentionally tiny: ->cache(), ->cacheForever(), and the CacheQuery facade.
  3. The facade (Laragear\CacheQuery\Facades\CacheQuery) — this is what you reach for to bypass caching in tests, admin panels, or post-write reads.

First use case

A read-heavy listing endpoint is the ideal first target. Wrap the terminal call on the query builder:

use App\Models\Product;

$products = Product::query()
    ->where('is_active', true)
    ->orderBy('name')
    ->cache()          // default TTL from config
    ->get();

That's it — the second identical request is served from cache. For a time-sensitive dashboard, pass a TTL:

$products = Product::where('is_active', true)
    ->cache(now()->addMinutes(5))
    ->get();

$categories = Category::orderBy('name')->cacheForever()->get();

Relationship queries work the same way, which makes per-parent caching trivial:

$user->posts()->where('published', true)->cache()->get();

Implementation Patterns

Cache at the query layer, not the response layer. The package keys off the SQL and bindings, so two different controllers issuing the same query share one cache entry. That is a feature — put ->cache() on the query builder in a repository/scope and every caller benefits without knowing about caching.

Pick a TTL strategy per query class. Reference data (countries, categories, feature flags) → cacheForever() plus explicit invalidation. Dashboards and listings → short TTLs (1–15 minutes) where staleness is acceptable. Detail pages → skip caching entirely; the hit rate is low and staleness is most visible there.

Use a dedicated cache store. Point cache-query.store at a separate Redis DB or a named store. Then php artisan cache:clear against that store only nukes query results, not sessions, rate limiters, or your Cache::remember payloads. It also makes flushing on deploy safe.

Invalidate with tags where the store supports them. Redis, Memcached, and array support tags; file and database do not (they'll throw or silently no-op depending on version). If your store supports tags, tag cache entries by domain (products, users:42) and flush those tags from model observers after writes. If it doesn't, either switch stores or rely on short TTLs — there is no automatic model-aware invalidation.

Bypass deliberately. Wrap requests that must see fresh data — post-checkout redirects, admin "view as user", anything that just wrote and re-reads:

use Laragear\CacheQuery\Facades\CacheQuery;

CacheQuery::avoid();          // this request skips the query cache
// ... your reads ...
// CacheQuery::allow();       // restore if needed

Same trick in setUp() of feature tests if tests share a real cache store — otherwise a cached row from one test leaks into the next and you chase ghosts for an afternoon.

Pagination is naturally per-page. paginate() adds limit/offset to the SQL before execution, so each page gets its own key. That's correct behavior, but it means changing perPage invalidates everything.

Watch the fan-out on bindings. ->where('email', $request->email)->cache() produces one cache entry per email. That's fine on a profile page and disastrous on a search endpoint. Cache queries whose bindings come from a small, closed set (tenant ID, category ID, status enum), not from free-form user input.

Combine with eager loading, not against it. Put ->with([...]) on the query before get(). The serialized collection includes the loaded relations, so cache hits return fully-hydrated graphs. Lazy-loaded relations are not cached and will still hit the database per request — the usual N+1, now hidden behind a cache hit.

Gotchas and Tips

  • No automatic invalidation. This is cache-aside. If a row changes, the cached collection keeps returning the old row until TTL expiry or an explicit tag flush. Model events, observers, and saved/deleted hooks are your invalidation layer — plan them in the same PR that adds ->cache().
  • Keys are SQL + bindings, not model-aware. Two model classes hitting the same table with identical SQL share an entry, even if their casts/accessors differ. If cached models are deserialized into a different class than you expect, this is why. Give such queries distinct tags or skip caching one of them.
  • Schema changes don't change the key. Adding a column leaves cached payloads that deserialize into models missing that attribute (it reads as null) until the TTL expires. When you ship a migration that adds columns the cached rows need, bump the cache-query.prefix (e.g., v2:) — old entries become orphans and age out naturally. Same applies when changing accessors, since accessor logic never appears in the SQL.
  • Don't cache inside uncommitted transactions. A rollback leaves the cache holding rows that never existed. Cache after commit, or guard with DB::afterCommit().
  • Empty results are cached too. A query that legitimately returns nothing caches an empty collection; if you're waiting for a background job to populate it, you'll serve emptiness until TTL. Use short TTLs on "expecting data soon" queries.
  • Place ->cache() last in the chain. It works earlier in most cases, but the readable, defensible convention is ...->orderBy(...)->cache()->get(). It prevents arguments about whether a ->where() added after ->cache() was included in the key.
  • Memcached's 1 MB per-item cap will bite you on large collections — the store rejects the item and you may see silent misses or an exception depending on driver version. If you're caching hundreds of hydrated models, measure the serialized size or cache IDs and re-query.
  • Debug with Laravel's cache events. Listen for Illuminate\Cache\Events\CacheHit / CacheMissed and dump the key — that tells you instantly whether the miss is due to a different binding, a different connection, or an avoid() left on from a middleware. CacheQuery::avoid() is also the fastest bisect: if disabling caching fixes a bug, the bug is staleness, not SQL.
  • Octane / long-running workers: the facade's avoid/allow state is per-worker static state. If you flip it in a request and don't restore it, the next request on that worker inherits the setting. Reset in a middleware terminate() or an Octane listener.
  • cacheForever() really means forever. There's no garbage collector. Pair it with tags, or accept that the entry lives until someone flushes the store.
  • Non-deterministic queries cache their randomness. inRandomOrder()->cache()->get() returns the same "random" order for every hit. Usually not what you want for a "surprise me" feature; usually exactly what you want for a randomized-but-stable A/B assignment.
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