andrey-helldar/cache
A lightweight Laravel cache wrapper that simplifies storing, retrieving, remembering, and forgetting cached values. It offers a clean API with TTL support, cache tags, and compatibility with multiple cache stores, making caching in apps and packages straightforward.
Install
composer require andrey-helldar/cache
Laravel auto-discovery registers the service provider and any facade alias — there is nothing to add to config/app.php. Confirm the support matrix before you commit to it in a legacy app:
composer show andrey-helldar/cache # requires / version / php floor
4.2.0 declares Laravel 13 support (added in #111) on top of the existing range. Treat the Laravel 13 claim as declared, not verified — nothing in the release notes evidences an L13 CI lane — so validate it with your own cache test suite before you rely on it during a framework upgrade.
Where to look first, in order
The README is thin and was recently rewritten for the "Smart Cache" rebrand. Do not treat it as the API reference. Read these instead:
vendor/andrey-helldar/cache/composer.json — the autoload.psr-4 namespace (confirm the rebrand did not change it) and extra.laravel.providers / aliases (tells you the provider class and the facade alias to use).config:cache drift (see Gotchas).@method docblocks — that is your real method surface: store, get, check, remove, TTL.config/cache.php in your own app — the package wraps Laravel's cache stores, it does not replace them. Your stores, prefixes, and driver configuration stay exactly where they are.Snippets below are illustrative of the shape of the API. Confirm exact method names against the facade docblocks before copy-pasting — a thin wrapper's README rarely matches its signature list.
First use case
Pick one hand-rolled cache block and collapse it. The pattern this package exists to remove:
// before
if (Cache::has($key)) {
return Cache::get($key);
}
$value = $this->expensiveQuery();
Cache::put($key, $value, now()->addMinutes(10));
return $value;
// after (illustrative)
return Cache::remember($key, $ttl, fn () => $this->expensiveQuery());
If that one-liner isn't meaningfully nicer than Illuminate\Support\Facades\Cache::remember(), stop here. The honest evaluation question for a wrapper package is: what does it do that the facade doesn't? Answer it on day one, on one endpoint, before you spread it across the codebase.
Sanity check
Run the same code path against two stores before shipping: array (what your test suite uses) and the store you actually deploy on (redis, database, file). A wrapper that behaves correctly on array can still surprise you on database because TTL and eviction semantics differ per driver.
Keep a thin boundary around it. Do not sprinkle the package facade through controllers and jobs. Put it behind one repository/service class per domain concern:
final class ProductCache
{
public function __construct(private ProductRepository $products) {}
public function find(int $id): ProductData
{
return Cache::remember($this->key($id), self::TTL, fn () => $this->products->find($id));
}
public function forget(int $id): void
{
Cache::forget($this->key($id)); // confirm method name
}
private function key(int $id): string
{
return "catalog:v1:product:{$id}";
}
}
This is the single highest-value pattern in this package's adoption: it's what makes the bus-factor risk (one maintainer, bot-assisted housekeeping commits) a non-event. Swapping back to Illuminate\Support\Facades\Cache becomes a one-file diff.
Key naming is your job, not the package's. The wrapper reduces boilerplate; it does not enforce discipline. Bake a convention into the boundary class and never let a raw key string escape into a controller: {domain}:v{n}:{entity}:{id}. The v segment is what lets you invalidate a whole namespace by bumping the version without needing tags.
TTL as configuration, not literals. Define TTLs as constants on the boundary class or entries in config/cache.php. 3600 appearing at eleven call sites is how you get an un-tunable cache. Two rough policies that hold up:
remember. Let expiry do the invalidation work.forget() on every write path, in the same transaction or immediately after commit.Invalidation on the write path. The failure mode that bites in production is stale data, not slow data. Every update/delete/create for a cached entity should call the boundary's forget(). If you have many related keys (lists, counts, aggregates), the version-key trick is portable across every store:
// reads: "catalog:v3:product:42" writes: bump the version
Cache::put('catalog:version', 4, null); // forever
Stock Laravel can do this too — but if this package gives you tagging, note that tagging is driver-dependent. It works on redis, memcached, and array; it does not work on file or database. If your tests run on array and production runs on database, tag-based invalidation will pass CI and silently fail in production. Version keys don't have that problem.
Testing. Set CACHE_STORE=array in phpunit.xml for speed, but keep one integration suite that runs against the real driver — that suite is where driver-specific TTL and tagging bugs surface. Flush between tests (Cache::flush() equivalent, or a fresh store instance in setUp) so hit/miss assertions aren't order-dependent. Write at least one test that asserts the end-to-end behavior — "second call does not hit the repository" — because that test is what protects you when you swap the wrapper out.
CI matrix during the Laravel 13 upgrade. The release notes show L13 support landed as a minor with no accompanying test-matrix detail. If you're upgrading the framework, run your cache suite on both Laravel 12 and 13 in the same pipeline rather than trusting the declared constraint. Also check your other illuminate/*-coupled packages in the same pass — trading one blocker for another is the classic upgrade-tax mistake.
Version pinning. Use "andrey-helldar/cache": "^4.2" and review minor-release diffs rather than auto-merging. For a single-maintainer dependency on the request hot path, a 30-second changelog read per release is cheap insurance.
The "Smart Cache" rebrand is a README change, not a package rename. 4.2.0's non-functional commits (#107–#110) are license year, README branding, and banner art. Before you copy an import from the README, verify the namespace in composer.json autoload.psr-4 still matches what you're pasting. A cosmetic rebrand that drifts documentation away from the actual namespace is the most likely source of "class not found" on first install.
config:cache drift. If the provider publishes a config file, any change to it requires php artisan config:clear (or a full re-deploy) in environments with cached config. A missing or renamed config key doesn't throw — it silently falls back to defaults, which is how you end up debugging a TTL that "isn't being respected" for an hour. Confirm whether a config file is published at all before you go looking for one.
TTL "forever" is the classic wrapper bug. Every store treats null, 0, and negative TTLs differently, and wrappers frequently normalize them differently from the facade. Read the TTL handling in source once, write a test for your "cache forever" case, and move on. Don't assume Laravel semantics pass through unchanged.
Closures can't be cached. The wrapper won't serialize them for you. Neither will it cast a cached Collection or Eloquent model back into the shape the caller expects. Store arrays/scalars (->toArray() at the boundary) and hydrate on read — otherwise the serialize/deserialize cost can exceed the query you were trying to avoid.
Cold-start stampede. A convenience wrapper gives you no atomicity guarantee. On a hot key that expires under load, N concurrent requests all miss and all recompute. If the key is expensive, wrap the compute in Cache::lock() yourself. This is not the package's job, and it's not something a remember() one-liner protects you from.
Debugging a miss. has() returning false conflates three different things: key is wrong, entry expired, entry evicted. Log the fully-resolved key string in dev at least once — most "cache isn't working" tickets are a key mismatch between the write path and the read path. Remember that your store prefix and the app-level cache prefix are both prepended; what you see in Redis is not what you typed.
There is no published benchmark. The layer is thin, so overhead should be negligible — but "should be" on the request hot path deserves ten minutes of before/after timing against your own store before a repo-wide rollout, not after.
Keep the escape hatch tested. Given single-maintainer governance and no LTS statement, the durable mitigation is a boundary class plus one behavioral test. When you eventually swap to native Laravel cache (or the package lags a future Laravel major), that combination turns a project into a contained afternoon.
How can I help you explore Laravel packages today?