simplesoftwareio/simple-cache
A lightweight Laravel helper for cleaner cache usage, wrapping the cache repository with simple remember and forget methods so you can store and retrieve values without repetitive boilerplate.
Install and pin expectations first. This is a 2016 Laravel 5.x-era package with 23 stars and zero dependents, so treat the source as the documentation:
composer require simplesoftwareio/simple-cache
Before writing any application code, open vendor/simplesoftwareio/simple-cache/src/ and read the trait end to end — it is small, and it is the only accurate reference you have. Confirm three things: the trait's namespace, the exact method names it adds, and whether it registers any model event listeners (most trait-based caches do their invalidation in boot{TraitName}()).
First use case. The trait's value is the boring one: a model you read constantly and write rarely. Attach it, cache a handful of lookups, and measure:
use SimpleSoftwareIO\SimpleCache\SimpleCache; // confirm path in src/
class Setting extends Model
{
use SimpleCache;
}
Then wire it into the one read path that is actually hot — a settings lookup, a currency table, a category tree. Do not retrofit it across every model on day one; the package predates $casts arrays, HasAttributes refactors, and PHP 8 return types, and you will find out quickly whether it survives contact with your Laravel version.
Verify the cache is real. Add a temporary Cache::getStore() dump and confirm you are hitting the store you think you are (array driver in tests silently passes everything). Then run a write and confirm the key actually disappears — Redis::keys('*') or php artisan cache:clear plus a re-read is enough.
Have an exit plan on the desk. If the trait does not boot on your framework version (likely on Laravel 9+ without patches), the same behaviour is ~30 lines:
Cache::remember("setting:{$id}", 3600, fn () => Setting::find($id));
Budget for that. It is a plausible outcome, and knowing it up front is why you read the trait first.
Wrap the read, not the model. The cleanest integration is to keep the trait on the model but funnel all reads through a repository or a scoped query method, so invalidation has a single place to live:
class SettingRepository
{
public function get(string $key): ?Setting
{
return Cache::remember("settings:{$key}", now()->addHour(), function () use ($key) {
return Setting::where('key', $key)->first();
});
}
public function flush(string $key): void
{
Cache::forget("settings:{$key}");
}
}
This survives the package being abandoned, which is the real risk here.
Let model events do the flushing. For models that do use the trait, register a saved/deleted observer that forgets the keys derived from the model — including any collection keys the record appears in:
static::saved(fn ($m) => Cache::forget("settings:{$m->key}"));
static::saved(fn ($m) => Cache::forget('settings:all'));
static::deleted(fn ($m) => Cache::forget("settings:{$m->key}"));
Note that saved fires on save() but not on Model::where(...)->update(...), DB::table(...)->update(...), increment(), or saveQuietly(). If your codebase uses any of those, add an explicit forget at the call site — this is the single most common source of stale reads in trait-based caching.
Cache the collection, not the rows. The pattern that pays off most is caching the assembled result your controller needs, because it removes N queries rather than one:
$tree = Cache::remember('categories:tree', 3600, fn () => Category::with('children')->get());
Row-level caching still leaves you with N hits and a hydration cost per row; result caching does not.
Version the keys. For a package this old, assume no tag support is wired up. Namespace keys with a cheap version bump so you can invalidate everything without a store flush:
$v = Cache::get('settings:version', 1);
Cache::remember("settings:v{$v}:{$key}", 3600, fn () => ...);
// invalidate all:
Cache::increment('settings:version');
Keep serialized models out of the cache where you can. Caching raw Eloquent models serializes the whole attribute set and any loaded relations; caching plain arrays or DTOs avoids surprises when the schema changes. If you must cache models, cache them with their relations explicitly eager-loaded and never rely on lazy loading after they come back out.
Test it with the array driver, and assert the negative. The valuable test is not "the value came back" but "after a write, the cached value is gone." Cache::spy() plus a saved assertion catches the mass-update gap above.
The maintenance risk is the headline. Last release December 2016, zero dependents, 23 stars. There is no Laravel 9/10/11 support, no PHP 8 type compatibility pass, no security response. Treat it as a code snippet you happen to install via Composer — vendor the trait into your own namespace if it works and you want to keep it.
Method collisions are the first failure you will hit. Traits that override getAttribute, newFromBuilder, or add find-style methods conflict with Eloquent internals that have changed substantially since 5.3. Compile-time fatal errors from trait conflicts are usually a good sign you should stop and inline the logic instead.
Mass updates bypass events. Model::where()->update(), DB::table()->update(), ->increment(), ->decrement(), ->delete() on a builder, and saveQuietly() all skip the model events your invalidation depends on. Audit every write path before trusting the cache. Wrap destructive statements in a helper that forgets the relevant keys as a rule.
Pivot and relation writes are invisible. attach, detach, sync, and updateExistingPivot fire different events than saved; anything caching a relation's contents needs its own forget call.
TTL is not a strategy. A one-hour TTL means up to an hour of stale data. Use it as a backstop for missed invalidation, not as the mechanism. Keep TTLs long and invalidation precise — the opposite configuration masks bugs and then surfaces them in production at the worst moment.
Debugging stale data. Check the driver first (config('cache.default') — an array store means nothing persists across requests and everything "works" locally). Then confirm the key exists (Cache::has($key), redis-cli KEYS '*'), then confirm the listener fires (Event::listen('eloquent.saved*', ...) and log). Nine times out of ten it is a key mismatch — a trailing colon, a different tenant prefix, or a now() in the key turning it into a nonce.
Multi-tenancy will bite. Any key must include the tenant identifier. Trait-provided defaults almost never do, so override the key derivation or wrap every access in a tenant-scoped repository.
Prefer maintained alternatives. genealabs/laravel-model-caching (automatic model-level invalidation), watson/rememberable (explicit remember() on query builders), or spatie/laravel-responsecache (HTTP-level) all solve this problem with current framework support. If you adopt this package, do it because the trait is tiny and readable, not because it is a dependency you want to grow into.
How can I help you explore Laravel packages today?