torann/taggable-file-cache
A Laravel package that adds tag support to the file cache driver, letting you group and invalidate cached items by tag—useful for lightweight file-based caching without Redis or Memcached.
Confirm it will even install (do this first). The only release is 1.0.0 from 2018-02-16, which puts it in the Laravel 5.5/5.6 era. Before writing a line of code:
composer require torann/taggable-file-cache --dry-run
composer why-not torann/taggable-file-cache
composer show illuminate/cache
Expect illuminate/* and PHP-floor conflicts on any modern Laravel. If it conflicts, take the credible path: copy the store class into app/Cache/ (BSD-2-Clause permits this with attribution) and register it yourself. The whole mechanism is one class — a FileStore subclass plus a TaggableStore implementation that writes tag-index files next to the payloads.
Where to look first. Read src/ in this order:
Cache::extend() driver registration.put(), get(), flush(), and whatever private method writes the tag index. That method is the package; everything else is plumbing.composer.json — the supported Laravel/PHP range you'll be overriding.Register the driver. If installing as a dependency the provider is auto-discovered; if vendoring, register it manually:
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Cache;
use App\Cache\TaggableFileStore;
public function boot(): void
{
Cache::extend('taggable-file', function ($app) {
return Cache::repository(new TaggableFileStore(
$app['files'],
storage_path('framework/cache/taggable'),
));
});
}
Then add a dedicated store in config/cache.php (do not change your default store):
'taggable-file' => [
'driver' => 'taggable-file',
'path' => storage_path('framework/cache/taggable'),
],
First use case: a coarse group flush. The canonical fit is something with a clear family of entries and a clear "everything in this family is now wrong" event — navigation/menu fragments, CMS blocks, dashboard widgets:
$menu = Cache::store('taggable-file')
->tags(['nav', 'locale:en'])
->remember("nav:en:{$userId}", 3600, fn () => $builder->buildMenu($userId));
// Later, on a content change:
Cache::store('taggable-file')->tags(['nav'])->flush();
Smoke test before you build on it. Write/flush/read in a test to prove the semantics and that the tag index actually gets cleaned:
$store = Cache::store('taggable-file');
$store->tags(['a', 'b'])->put('k', 'v', 600);
$this->assertSame('v', $store->tags(['a'])->get('k'));
$store->tags(['b'])->flush();
$this->assertNull($store->tags(['a'])->get('k'));
Always pin the store explicitly. Your default cache driver is plain file and not taggable; a bare Cache::tags() throws BadMethodCallException. Centralise the handle so nobody forgets:
// app/Support/TaggedCache.php
final class TaggedCache
{
public static function store(): Repository
{
return Cache::store('taggable-file');
}
}
Design tag sets as an "entity + facet" taxonomy. Write with the full set, flush with the smallest overlapping set — the index matches on any flush tag:
TaggedCache::store()->tags(["product:{$id}", 'catalog', 'search'])->put(...);
// one product changed
TaggedCache::store()->tags(["product:{$id}"])->flush();
// bulk import finished
TaggedCache::store()->tags(['catalog', 'search'])->flush();
Keep the number of tags per entry small (2–3). Every tag on a write is another index file to maintain and another place to orphan a key.
Invalidate from model events, not from controllers. The flush should live wherever the truth changes:
protected static function booted(): void
{
static::saved(fn (Product $p) => TaggedCache::store()->tags(["product:{$p->id}"])->flush());
static::deleted(fn (Product $p) => TaggedCache::store()->tags(["product:{$p->id}"])->flush());
}
Wrap the whole thing in a TaggableCache service (or a small InvalidatesTags trait) so the tag-naming convention lives in one file instead of 40 call sites.
Use cache-aside with remember, never manual has/get/put. remember() keeps the tag-write path in one place and avoids the classic bug where a put without tags writes an entry no flush can ever remove.
Version-prefix escape hatch. For one-dimensional invalidation you don't need tags at all — bump a version string in config/settings and let the old keys age out via TTL:
$key = 'nav:'.$settings->menuVersion.':'.$userId;
Reach for tags only when a set of unrelated keys must die together. This is the honest comparison, and it's usually cheaper.
Testing pattern. Point the store at a per-test temp directory (or call ->flush() in setUp) so tests are hermetic; then assert both the hit and the post-flush miss. Add a concurrency test — parallel puts plus a flush() — because tag-index bookkeeping is exactly the thing that fails silently.
Operational pattern. Add a scheduled janitor command that clears the taggable path on a cadence and logs directory size, so orphaned index files don't creep. Pre-warm the hot tags after deploy; a cold file cache on a single node is a latency spike.
It is a 2018 artefact. Assume composer require fails on current Laravel. Pin exactly — "1.0.0" or a commit SHA or a vendored copy — never ^1.0. There is one release ever, so a floating constraint buys you nothing and imports unknown breakage. Record it in the tech-debt register as an internally owned component.
Node-local invalidation is the correctness ceiling. flush() only clears the machine that ran it. On any multi-server, autoscaled, or container deployment, sibling nodes keep serving stale entries — a silent, user-visible bug. This caps the package to single-node or sticky-routed setups. If Redis is on the roadmap, adopting this buys little and creates a migration later. Keep call sites on the framework-native Cache::tags([...]) API so the eventual Redis switch is a driver swap in config, not a refactor — do not let anyone call package-specific methods.
Disk read-modify-write races. Tag index writes need locking discipline. Concurrent writes and flushes can orphan entries or prematurely invalidate. Verify whether the store implements LockProvider and, if the invalidation matters, wrap the critical section with Cache::lock(). Even then: do not use this for correctness-critical data (entitlements, pricing, permissions). Add TTLs as a backstop so a missed flush degrades to staleness, not permanent wrongness.
Index files grow unbounded. A crash mid-flush leaves an index behind; entries that expire by TTL leave keys in the tag index. Budget a janitor command and a size alarm.
Deploy-time wipes. php artisan cache:clear and deploy scripts can wipe payloads while index files survive (or vice versa, depending on paths). Keep the taggable path in its own directory, outside whatever a deploy artifact replaces, and accept a cold start or warm it deliberately.
Serialization and payload size. The file store serializes on put. Closures and non-serializable objects blow up; large payloads mean disk I/O per read. This is a performance regression versus in-memory stores — cache fragments, not whole rendered pages, and keep throughput modest.
Debugging by looking at the disk. When a flush "doesn't work", ls -la storage/framework/cache/taggable and cat the index files. Confirm (a) the entry exists under your prefix, (b) the index file lists the key, (c) the tag string matches exactly — product:42 and products:42 are different tags and a typo produces a perfectly silent no-op. Give the store a distinct prefix so its files are identifiable among other cache usage, and log tag flush calls behind a debug middleware while you're establishing trust.
Octane / long-lived workers. The cache manager memoises store instances, so config and path changes need a worker restart. Ensure the store holds no request-scoped state between requests.
Extension points. Subclass the store to add a flushStale() janitor, override key normalisation or getCachePath() to control the file layout, and add a Cache::extend variant that swaps the tag-index serialiser if you need one. The most valuable extension is the one around it: your own TaggableCache facade, so replacing the driver with Redis later touches one class.
How can I help you explore Laravel packages today?