laravel-enso/rememberable
Laravel Enso Rememberable adds transparent query caching to Eloquent models via a trait. Cache results with optional tags and automatically invalidate them when models are created, updated, or deleted, reducing database load with minimal code changes.
Install
composer require laravel-enso/rememberable
The package is a thin trait library — no service provider bootstrapping beyond package discovery, and (depending on version) an optional rememberable.php config you can publish with php artisan vendor:publish --tag=rememberable-config. Verify the tag by running php artisan vendor:publish and picking the package from the list.
Configure a real cache store first. This package is only as good as the store behind it. Use redis (or memcached) in .env:
CACHE_DRIVER=redis
File/database stores will work but are far slower and may not support the tagging/invalidation behavior you expect.
Look here first, in this order:
src/Traits/Rememberable.php — the public API (remember(), cache(), forget(), and the key builder). This is the whole package; read it in five minutes.src/Traits/Cacheable.php (if present) — the model-event hooks that auto-invalidate on saved/deleted.config/rememberable.php — default TTL and cache prefix.README.md — examples of the intended call signature.First use case — cache an expensive accessor. Add the trait to a model and wrap the computation:
use LaravelEnso\Rememberable\Traits\Rememberable;
class User extends Model
{
use Rememberable;
public function getStatsAttribute(): array
{
return $this->remember('stats', fn () => [
'orders' => $this->orders()->count(),
'spent' => $this->orders()->sum('total'),
], now()->addMinutes(10));
}
}
The first call hits the DB; subsequent calls return the cached array until TTL expiry or explicit forget('stats').
When it's a fit: small/medium apps that want per-model caching without pulling in a full cache-decorator library. When it isn't: you need tag-based invalidation across many models, or your cache store can't support it — the package is 28 stars and low-activity, so treat it as a readable utility, not an actively maintained dependency.
1. Cache computed/aggregate attributes, not raw rows. The sweet spot is expensive derivations: counts, sums, formatted payloads, permission flags. Caching whole Eloquent models works but serializes everything and goes stale the moment any column changes.
public function getBadgeAttribute(): string
{
return $this->remember('badge', fn () => match (true) {
$this->orders_count > 100 => 'gold',
$this->orders_count > 10 => 'silver',
default => 'bronze',
});
}
2. Pair Rememberable with Cacheable for automatic invalidation.
If the package ships the Cacheable trait, use both. Cacheable hooks saved/deleted and flushes that model's keys. Without it you must call forget() manually in every mutation path — a reliable source of stale data.
class User extends Model
{
use Rememberable, Cacheable;
}
3. Invalidate related models from observers.
Caching a user's stats means an Order save should flush the owning user's cache. Wire it in an observer:
class OrderObserver
{
public function saved(Order $order): void
{
$order->user?->forget('stats');
}
}
This is the pattern that actually keeps caches correct — the trait only gives you the primitives.
4. Scope keys by tenant/user when the data is scoped.
Default keys are usually class:id:key. In multi-tenant apps, override the key builder so the tenant is part of the key, otherwise tenant A reads tenant B's cached attribute:
protected function cacheKey(string $key): string
{
return sprintf('%s:%s:%s:%s', tenant()->id, static::class, $this->getKey(), $key);
}
(Confirm the exact method name in the trait — it's the single most important thing to override.)
5. Use a short TTL as a safety net, long TTL + explicit invalidation as the goal. Set a sane default in config (e.g. 1 hour) and pass shorter/longer TTLs per call. Treat TTL as a backstop for missed invalidations, not the primary correctness mechanism.
6. Warm caches in a queued command, not in web requests. Because the first call is a miss, cache warming avoids slow first-page loads:
User::chunk(200, fn ($users) => $users->each->getStatsAttribute());
Run it post-deploy and after bulk imports.
7. Cache at the resource/API layer, not the ORM, when responses span many models.
Rememberable is model-scoped. If your response combines several models, cache the final array in a dedicated service class and invalidate it from observers — don't try to compose model-level caches into one payload.
User::where(...)->update(...), insert(), and raw SQL never fire saved, so Cacheable won't flush. Either invalidate manually in the same job or avoid caching columns touched by bulk queries.forget() calls in the pivot mutation path.Cache::tags(...), redis/memcached are required; file/database/array drivers will throw or silently misbehave. Check before you ship.stats key and the same id will stomp each other otherwise.null is sticky. remember() caches falsy values too. If an accessor legitimately returns null while data loads asynchronously, you'll serve null until TTL. Use Cache::has() checks or short TTLs for that path.rememberable.php, run php artisan config:clear (or config:cache on deploy) — otherwise you'll debug a config change that never took effect.array cache driver and flush between tests, or cached values leak across test cases and cause phantom failures:
protected function setUp(): void
{
parent::setUp();
Cache::flush();
}
$model->cacheKey('stats') or equivalent), then Cache::get($key) and Cache::forget($key) in tinker. Nine times out of ten it's a key mismatch or a missed observer, not the trait.composer.json.How can I help you explore Laravel packages today?