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

Rememberable Query Laravel Package

darkghosthunter/rememberable-query

A Laravel package that caches Eloquent query results and remembers them across requests, with automatic invalidation on model changes. Drop-in trait for faster repeated queries.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and sanity-check first. The package is archived (last release Feb 2022), so treat installation as a compatibility exercise before anything else:

composer require darkghosthunter/rememberable-query

Then immediately verify it works on your Laravel version — the package predates Laravel 10/11, and builder macros registered the old way are the most likely breakage point. In a scratch app, run one query through it and confirm the second call hits the cache (watch DB::listen or your query log). If it doesn't, stop here and pick an alternative rather than debugging a dead repo.

Where to look first.

  1. README.md — the whole surface area is small; the expressive one-liner is the product.
  2. src/ — find where the Eloquent/Query Builder macro or trait is registered. This tells you whether it patches the query builder, the Eloquent builder, or requires a trait on each model. That single detail determines every pattern below.
  3. config/rememberable-query.php (publish with php artisan vendor:publish) — look for cache store, key prefix, and default TTL. Set the prefix to something app-specific so you can identify and purge these keys later.

First use case. Pick a genuinely expensive, genuinely read-only query — a dashboard aggregate, a reference/lookup table, a sitemap or navigation tree. Don't start with anything a user can edit, because the first thing you'll hit is stale data and you want that to happen somewhere harmless:

// e.g. — confirm the exact signature against the README
$stats = Order::query()
    ->where('created_at', '>=', now()->subDays(30))
    ->remember(now()->addMinutes(10))
    ->get();

Then change the underlying data and watch the response not change. That's the contract you're signing up for.

Implementation Patterns

Cache at the query-object boundary, not inline. The value of a one-liner is that it composes with your existing query building. Put it at the end of a scope or a dedicated query class so the TTL lives next to the thing it describes:

class ActiveCountriesQuery
{
    public function get(): Collection
    {
        return Country::query()
            ->where('active', true)
            ->orderBy('name')
            ->remember(3600)
            ->get();
    }
}

This keeps the cache decision in one reviewable place instead of scattered across controllers.

Use it for reference data and expensive aggregates. The package shines on tables that change on a schedule (currencies, tax rates, feature flags) and on rollups that are expensive but tolerance-for-staleness is measured in minutes. It's a poor fit for anything in a user's own write path.

Warm the cache out of band. Pair the query with a scheduled command that calls the same query object every N minutes, so no user ever pays the cold-start cost and the TTL effectively never expires during traffic:

$schedule->call(fn () => app(ActiveCountriesQuery::class)->get())->everyTenMinutes();

Bust explicitly where TTL isn't enough. If the package doesn't expose a forget API (check src/), keep a version segment in your own key strategy or call Cache::flush() scoped to a prefixed store. Alternatively, hook model events:

Country::saved(fn () => Cache::store('rememberable')->flush());

Crude, but honest — and better than a TTL that's wrong for the business.

Eager-load before remembering. with() doesn't change the SQL that generates the cache key, but it does change what gets serialized into the cached collection. Always with() the relations you actually need inside the remembered query; otherwise you'll cache bare models and lazy-load on every request anyway, which defeats half the point.

Treat the returned collection as immutable. Whatever you get back is a hydrated snapshot. Don't mutate models and save() them expecting the cache to know — call fresh() or re-query after writes.

Gotchas and Tips

It's archived — budget for that. No security fixes, no Laravel version support, no issue triage. If the package is load-bearing, vendor the source into your app or fork it. The logic is small enough that Cache::remember() around a toSql()/binding hash is a 20-line reimplementation you fully control — seriously consider that instead of the dependency.

Cache keys are usually derived from SQL plus bindings, and that's leaky. Two connections with identical SQL and bindings (multi-tenant, separate databases) can collide. Verify whether the key includes the connection name; if not, add your own tenant/database discriminator via the key prefix or a forced binding.

Pagination needs the page in the key. If toSql() includes the limit/offset it's fine, but confirm it — a cached page 1 served as page 2 is a nasty, subtle bug. Safest: never remember a paginated query, or hash the full request state yourself.

Soft deletes and global scopes are your friend here. Because they alter the SQL/bindings, the key changes automatically when the scope changes. That's the good news. The bad news is it means a scope that varies per-request (e.g. where('user_id', $id)) fragments your cache into one entry per user — usually not what you want. Scope the cache to genuinely shared data.

Staleness is invisible until it isn't. Cache hits look exactly like fresh data in your code. Log cache hits/misses in staging, and add a test that asserts the data updates after the TTL — otherwise the first stale-data incident will be diagnosed by a very confused person in production.

Testing. Use the array cache driver and Cache::flush() in setUp(). Add at least one test asserting the query is not re-run on the second call (spy on DB::listen), and one asserting your invalidation path actually clears it.

Debugging. If results look stale but you can't tell if it's the cache or the DB: Cache::getStore(), dump the resolved key, and compare toSql() + bindings between the two calls. Nine times out of ten it's a differing binding (a now() or a float) producing a fresh key every time — i.e. a 0% hit rate — which looks like "the cache isn't working" when it's actually working too well.

Extension points are minimal. Beyond the config file and whatever the macro registration does, there's little to hook into. Plan on wrapping it rather than extending it.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource