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

Multi Level Cache Laravel Package

tbessenreither/multi-level-cache

A Laravel package for multi-level caching that layers fast in-memory, file, and distributed stores behind one API. Read from the fastest level, fall back to slower ones, and optionally promote hot keys upward to cut latency and backend load. Fully configurable drivers and TTLs.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install and publish config:
    composer require tbessenreither/multi-level-cache
    php artisan vendor:publish --tag="multi-level-cache-config"
    
  • Open config/multi-level-cache.php first. That file is the whole mental model of the package: it defines which cache stores are stacked (typically a fast in-process/array or file L1 in front of Redis/database L2), their TTLs, and the key prefixing strategy. Read it before touching any service class.
  • Confirm your app's default cache store and the package's L2 point at the same backend — a mismatch here is the most common cause of "cache works but invalidation doesn't."
  • First use case: cache an expensive but infrequently changing read (dashboard aggregates, settings payloads, API lookups) rather than per-request session data. Start with one endpoint, measure hit ratio, then expand.
  • After upgrading to v1.4.2, regenerate your cached services (the release adds a mixin to the generated service stubs). Look at a generated service before and after regeneration to see the new extension point, and check the Invalidator service class since its internals changed.

Implementation Patterns

Cache-aside on the multi-level manager. Wrap the expensive path in a remember-style call on the package's cache manager so reads hydrate L1 from L2 transparently. Keep TTLs short on L1 and long on L2 — L1 is the layer most likely to drift.

Generated cached services as the default unit of work. Prefer the package's generator over hand-written cache blocks for anything reused. Each generated service becomes the single place where a key, TTL, and invalidation contract live, which is far easier to review than scattered Cache::remember calls.

Use the new mixin instead of editing generated files. v1.4.2's mixin is the intended extension point: compose extra behavior (logging, metrics, tenant key scoping, custom serialization) onto a generated service without modifying the generated class body. Treat generated services as build artifacts — regenerate freely, keep customization in the mixin. If the mixin doesn't cover your case, wrap the service in a decorator rather than forking the stub.

Invalidate through the invalidator service, not by forgetting keys. The improved invalidator in v1.4.2 is designed to evict across levels in one call. Wire it to domain events (Model::saved, deleted, or your own domain events) so cache correctness is a consequence of the event flow rather than a checklist developers must remember. Always invalidate after the write commits — invalidating inside a transaction that later rolls back leaves you serving data that never existed.

Bind the invalidator behind an interface in a service provider. Injecting an interface lets tests substitute a no-op or recording fake, and lets you swap in a queued/async implementation later without touching call sites.

Queue large invalidations. For bulk operations (imports, mass updates) that touch thousands of keys, dispatch invalidation to a job. Doing it synchronously in a loop is the usual cause of a slow admin action.

Testing pattern. Use the array store for both levels in phpunit.xml, assert the underlying query runs once across two calls, then fire the invalidation event and assert it runs again. That single test catches most multi-level regressions.

Integration tip. If you run Octane, Horizon, or long-lived queue workers, ensure invalidation reaches L1 in those processes too — either via a very short L1 TTL or an event/broadcast that clears local state. This is the pattern most teams skip.

Gotchas and Tips

  • L1 is per-process. In Octane/FPM-pooled or queue-worker contexts, invalidating L2 does not clear another process's in-memory L1. Keep L1 TTLs aggressively short (seconds, not minutes) or use a shared L1 store until you've wired cross-process invalidation.
  • Regeneration overwrites generated services. Never hand-edit them; your changes vanish on the next make or upgrade. Everything custom belongs in the v1.4.2 mixin or a decorator.
  • Regenerate after upgrading to v1.4.2. Existing generated services predate the mixin and the updated invalidator wiring. Diff one regenerated service against your current one before rolling out broadly, and check whether any customized stubs conflict.
  • config:cache will bite you. After changing cache levels, TTLs, or prefixes, run php artisan config:cache in deployment and config:clear locally. Stale cached config masquerading as a code bug is the classic false alarm here.
  • Two cache stores means two serialization paths. Values that work in L1 (arrays, objects) may fail or change shape through L2. Stick to plain arrays/scalars or DTOs with explicit (de)serialization; closures and resources are never cacheable.
  • Key collisions across levels. Ensure the configured prefix is set per environment. Sharing a Redis instance between staging and production with the same prefix will produce baffling cross-environment staleness.
  • Debugging misses. Temporarily log on hit/miss at both levels and watch the L1→L2→source fill sequence for one request. If you never see an L2 hit, your L1 TTL is likely longer than your L2 TTL, or invalidation is clearing both on every write.
  • Invalidation timing. Fire the invalidator in afterCommit (or after the transaction closes), never inside it. Also invalidate on bulk paths that bypass Eloquent events — raw DB::table() updates silently skip event-driven invalidation.
  • Extension points to know: the invalidator binding in the container, the generated-service mixin, and the published config. Those three are the intended surface area — prefer them over subclassing internal classes, since v1.4.2 shows the internals of the invalidator service are still moving.
  • Pin the version. This is a minor release with behavioral changes to a core service. Use a caret constraint and read the changelog on each bump; test invalidation explicitly rather than assuming correctness.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core