All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Membership revalidation: a cached membership survives a version bump when every version in the gap was a precise
UPDATEwhose columns are disjoint from the query's predicate and order columns. Configure withrevalidation, and declareprotected array $volatileColumnsfor columns the database writes without the statement naming them. - Automatic result overlays: eligible canonical queries store a complete result payload for faster warm reads and rebuild it from canonical rows when the overlay is missing.
- Unified query controls:
dependsOn()accepts models and table names,tag()groups related queries,NormCache::invalidate()accepts model or table targets, andNormCache::withoutCache()runs a whole callback, eager loads included, against the database. - Runtime cache switch:
normcache:disableandnormcache:enable, plus their facade equivalents, pause and safely resume caching across application nodes. - Database source scopes:
database.connections.<name>.normcache_scopeisolates shards or tenants and allows aliases for the same source to share cache state deliberately. - Serializer selection: choose
auto,php, origbinarypayload serialization.
- BREAKING cache layout: cache spaces were removed and Redis placement is now derived from physical tables and query groups. Cache keys changed, so v3 and v4 must not serve traffic together. Run
normcache:disable, deploy every web and worker node, then runnormcache:enable. - BREAKING API consolidation: replace
dependsOnTables()withdependsOn(), legacy invalidation methods withNormCache::invalidate(), and model-scoped tag flushing withflushTag('name'). - BREAKING flush behavior:
normcache:flushnow always performs a global invalidation;--modeland--spacewere removed. - Model attribute lifetime is configured with
row_ttl/NORMCACHE_ROW_TTLrather thanttl/NORMCACHE_TTL, and the key-prefix variable is nowNORMCACHE_KEY_PREFIXrather thanNORMCACHE_PREFIX.
- Improved cache isolation for connection aliases, shards, tenants, and runtime database switching.
- Manual and automatic invalidation now handle connection-aware table names, unknown write targets, and writes that throw after reaching the database safely.
- Improved build-lock and Redis reconnect behavior when responses are lost or operations are retried.
- Cache spaces,
$normCacheSpaces,space(), thespacesconfiguration, and space-targeted flushing. - Legacy
cooldown,fallback, andfire_retrievedoptions,Builder::explain(), and the cache-manager facade accessors (modelCache(),resultCache(),versionStore(), and friends). - The
ModelCacheHit,ModelCacheMiss, andCacheMetricRecordedevents. - The
stampede_wake_tokensoption andNORMCACHE_STAMPEDE_WAKE_TOKENS; the wake-token count is now fixed. - Environment overrides for build-lock tuning: set
building_lock_ttlandstampede_wait_msin the published config file, notNORMCACHE_BUILDING_LOCK_TTL/NORMCACHE_STAMPEDE_WAIT_MS. - The relation-specific cache classes; relation and pivot reads are cached through the same query path as everything else.
CacheInvalidatedandCacheMetricRecordedevents whennormcache.eventsis enabled. Cache hit and miss events now include cache metadata.
- Debugbar records overall cache-operation duration only; component-specific timing breakdowns are no longer collected.
- Internal cache execution and invalidation code has been consolidated behind dedicated services.
useWritePdo()reads, including relation reads, now bypass the cache and preserve read-your-writes behavior.- Cache keys for root, through, and pivot reads now consistently use the active Eloquent connection, preventing cross-connection cache leakage.
- Model updates now invalidate before and after the write, so a concurrent pre-write read cannot remain reachable after a successful update.
- Index, through, and pivot cache hits preserve the Lua-resolved model version when fetching model payloads.
- Atomic cache spaces for Redis Cluster: declare
$normCacheSpaces, select one with->space(), and run cache operations in one hash slot. - Space-targeted flushing: use
NormCache::flushAll('space')orphp artisan normcache:flush --space=.... - Cache-space coverage: relations, pivot data, and table dependencies participate in cache-space invalidation.
- BREAKING: model-declared cache spaces replace the
clusterandslottingconfiguration. Runphp artisan normcache:flushbefore deploying. dependsOnTables()supports named cache spaces.
- Cache isolation across declared spaces and database connections, including
Model::on()queries and writes. - Repeated
dependsOn()calls no longer discard previously declared model dependencies. - Stale cache entries are not written when a cache rebuild races an invalidating write.
- Table-dependency invalidation remains visible across application workers.
- Eloquent observers see invalidated cache state while a model is being saved.
- Dropped stale-serving and the
stale_version_depthconfig option. It did shave latency off reads during a rebuild, but readers already wait on a wake channel in that window — the added complexity wasn't worth maintaining for that marginal win, so the build-lock path is simpler now.
- A malformed scalar/count/result cache entry (wrong shape for its kind) no longer recomputes from the database on every single read until TTL expiry — the first read after corruption now repairs the entry.
- Pivot cache (
belongsToMany/morphToMany) build-lock stampede protection: concurrent misses on the same batch wait on a wake signal instead of racing the database. stampede_wake_tokensconfig (default64): wakes N waiters per completed build instead of 1.- Stale-serve (depth-bounded backward version search) extended from single-dependency queries to multi-dependency query, through, and result caches.
fallbackdefault:false→true(fails open to the database on Redis errors). SetNORMCACHE_FALLBACK=falseto restore fail-closed behavior.- Removed
inline_model_thresholdconfig and the inline model-fetch path in the query/through Lua scripts; models now hydrate from cache in PHP after the Lua round trip.
- Cluster fixed-hash mode (
cluster=true,slotting=false) now uses the atomic multi-key Lua path instead of the per-key fallback. fetch_model_build_status.luachunksMGETat 500 keys/batch, fixing a Lua/Redis argument-limit error on cold misses ≥500 ids.- Cold-miss re-query on joined tables now selects
{table}.*instead of*, fixing an ambiguous-column error.
- Relation definitions with built-in
whereExists, raw predicates, or non-inferrable joins now bypasswhereHas/withCountinference instead of caching under incomplete dependencies. count(),exists(),sum(),avg(),min(),max(), andpaginate()with a manualwhereExistsnow bypass unlessdependsOn()is declared.lockForUpdate()andwithoutCache()inside awhereHasconstraint now prevent the outer query from being cached.- Plain
join()auto-inference extended to scalar and pagination paths, and now correctly bypasses on complex join predicates, implicit table aliases, or insidewithCount()/withAggregate(). - Through-relation
tag()andttl()now applied to result fetch and store calls. - Pivot-relation
ttl()now applied to result writes;tag()bypasses pivot caching (key structure does not support tag namespacing). - Nested eager loads on a cached
belongsTorelation (e.g.with('order.customer')) are now hydrated instead of being silently dropped. latestOfMany()/oldestOfMany(),hasOne, andhasOneThroughrelations now correctly track their cache dependency.- Fixed cross-slot
EVALSHAerrors when running against a real Redis Cluster with multi-dependency queries.
- Reduced Redis round trips for cold cache misses across model hydration, relation loading, and the build-lock recheck.
- Cache hits on query/through/pivot lookups now decode their JSON payload in PHP instead of in Lua, avoiding Lua's slower bulk-reply marshaling on the warm path.
- Simple
whereHas/whereDoesntHavecaching:whereHas,orWhereHas,whereDoesntHave, andorWhereDoesntHaveare now cached automatically — nodependsOn()required. Works for single, non-nestedCacheablerelations with safe constraint closures. Nested relations,MorphTo, and unsafe constraints still bypass. - Automatic join table inference: plain
join()calls automatically add the joined table as a cache dependency. Use an explicit root-table projection (e.g.select('authors.*')) to enable result caching;SELECT *joins still bypass.
- Through-relation cross-parent cache collision:
hasManyThrough/hasOneThroughno longer share a cache key across different parent models. Previously two parents (e.g. two countries) could receive each other's cached results. - Through-relation
dependsOn()dependencies ignored: extra dependencies declared on a through-relation query are now included in the version keys and will correctly invalidate the cache. - Pivot relation with explicit
dependsOn()bypasses instead of caching: pivot caching cannot track extra dependencies in version keys, so it now bypasses rather than risk stale results. - Through simple guard misses non-canonical
from: the fast-path check now compares against the actual related model table, so aliased or replacedfromsources are correctly detected and bypass the cache.
- Fixed cache-key collisions between
sum,avg,min,max, andexistsaggregates. - Through-relations with raw
wherebindings or subquery/whereExistspredicates now bypass caching instead of risking collisions and stale reads. value('column as alias')no longer errors; it returnsnull, matching native Eloquent.tag('')is now rejected.- Made the
normcache.eventsdefault consistentlyfalseacross config, provider, and README. - Fixed the under-declared-dependency warning to reference
dependsOnTables().
dependsOnTables()now rejects reserved key characters in table names.normcache:flushoutput now says "NormCache key(s)" rather than "model cache key(s)".
- BREAKING — cache events are now opt-in: the
normcache.eventsdefault changed fromtruetofalse. - Pivot cache batching: batch pivot cache writes during eager loading to reduce Redis roundtrips.
- Hydration optimization: implemented closure-based hydration and removed expensive regex from unserialization for faster model loading.
- Dedupe transaction bumps: reduced redundant Redis calls by deduping version bumps within the same database transaction.
- Reporting overhead: model-hit key collection during result hydration is skipped when neither events nor a Debugbar collector are active.
- Result cache correctness: fixed edge cases where result cache could return stale or incorrect data.
- Wildcard alias support: improved handling of queries using wildcard aliases in the result cache.
- Model hydration: fixed issues with closure-based model hydration when restoring certain attribute types from cache.
dependsOnTables(array $tables): declare raw table names as cache dependencies alongsidedependsOn()model classes. Useful for queries that touch tables without a corresponding Cacheable model.- Dependency completeness warnings: when
dependsOn()ordependsOnTables()is used, the planner warns at query time if the declared dependencies do not cover all tables referenced in the query, surfacing likely invalidation gaps early. chunk(),each(), andlazy()bypass cache: streaming operations always execute against the database; caching partial result windows is not meaningful.sole()bypasses cache: must verify live row count against the database; a cached snapshot could incorrectly suppress or pass the uniqueness assertion.
- Query hash includes model casts: queries on models with different cast configurations no longer share a cache key when the underlying SQL is identical.
- Atomic version bump with TTL refresh: version increments execute
INCR + EXPIREatomically, preventing counters from becoming permanent when a TTL-expired key is incremented. - Multi-dependency cluster routing: normalized queries with more than one dependency model or table are routed to the result cache in cluster mode, where each version key resolves to its own slot.
- Internal structure: cache execution, column projection, query hashing, and Redis serialization have been reorganized for consistency across all cache paths.
deleteQuietly()invalidates cache: now triggers the same flush asdelete().skipCachepropagation: nested relation queries no longer inherit the flag through shared builder state.- Scalar corrupt-payload fallback: a stored scalar with an unexpected shape falls back to a live query instead of returning a wrong value.
- Relation projection accuracy: through, pivot, and
belongsTorelations with column constraints no longer write or read payloads built from the wrong column set. - Pivot constraint hash stability: constraint bindings are now serialized to a stable representation across PHP versions.
SSCANdouble-prefix: member keys were being double-prefixed during model set flush scans.
- Cross-slot violations: optional keys (building, wake) now share the primary hash tag so Lua scripts stay within a single slot.
- Predis cluster script loading: Predis cluster raises
NotSupportedExceptioninstead of a NOSCRIPT error when a script isn't cached on a node; this is now caught and falls back toEVAL. - Connection prefix detection: correctly reads the configured prefix for both standalone and cluster clients.
- Transaction instance eviction: a single-row save inside a transaction now evicts only that model's payload on commit instead of flushing the entire model class.
- Result cache projection:
get(['col'])on a result-cached query now hashes and fetches with the correct column set instead of silently usingselect *. - Aggregate constraint dependencies: extra model and table dependencies declared inside a
withCount/withSumconstraint are now merged into the aggregate dependency set. - Through/pivot projection guard:
HasManyThrough,HasOneThrough, and pivot relations now bypass cache when the related primary key is not in the selected columns.
remember()— replaced byttl(). Use->ttl(300)to set a custom cache duration.
- Planner-driven caching: query safety, dependency inference, bypass reasons, and cache-mode selection now flow through explicit planning classes.
dependsOn(array $modelClasses): cache cross-table queries by declaring which model classes can invalidate them. Simple dependency-aware queries stay normalized; complex query shapes use a versioned result cache.- Versioned result cache: complex dependency-aware queries, relation aggregates, scalar values, pagination counts, pivot reads, and through-relation reads share versioned invalidation and stampede handling.
- Scalar result caching:
count,sum,avg,min, andmaxare cached under versioned keys and invalidated with their parent and declared dependency models. - Relation aggregate caching:
withCount,withSum,withAvg,withMin,withMax, andwithExistsare cached as versioned result payloads and invalidated by inferred or explicit dependencies. MorphToeager-load caching: each morph type is served from the model cache when the relation can be safely cached.Builder::explain(): returns a string describing why a query is cached or bypassed.- Debugbar integration: hits, misses, timeouts, and bypasses appear on the Debugbar timeline when Debugbar is installed.
- Manual invalidation tags:
tag(),flushTag(), andflushTagAcrossModels()group query entries for manual flushing. - Stampede protection: cache builders coordinate with build locks and wake channels; waiters can serve configured stale versions or take over orphaned builds.
stale_version_depthconfig: controls how many stale versions to serve during stampede protection. Default:3, set to0to disable.- Queue worker and Octane recovery: cache state is reset between jobs, Octane requests, and Octane tasks.
- Cache-mode selection: normalized cache is kept for simple primary-table queries, including simple queries with declared dependencies; result cache is used for dependency-aware complex queries and result-style operations.
- Redis Cluster support: cross-model paths (
dependsOn, pivot, through,withCount) resolve each model's version key individually per slot. Single-instance behaviour is unchanged. Enable withNORMCACHE_CLUSTER=true. - Aggregate caching simplified: relation aggregates are cached as a versioned blob per query. The per-parent-ID aggregate key structure and old aggregate loader path have been removed.
- Invalidation coordination: distributed write locks have been replaced by version and CAS-based coordination.
- Internal structure: relationship helpers, cache reporting, Redis scripts, query hashing, result payload projection, and builder invalidation code have been reorganized around the new planner and versioned cache flow.
- CAS-protected writes: prevent stale query, result, scalar, pivot, and model data after concurrent invalidation.
- Cooldown invalidation across all cache families: scalar, result, and pivot scripts now apply scheduled invalidations before reading, matching the query cache behaviour.
- Redis flush paths:
flushAll()and model flushes useSCAN/SSCANinstead of loading full key/member sets. - Mutable primary keys: changing a model's PK via
save()now evicts the oldmodel:{table}:idcache key. - Raw build lock tag-segmented: different tagged queries no longer share the same stampede lock.
where/whereRawon aggregate alias falls back correctly: these patterns now trigger the native Eloquent path instead of running a broken ID query.withAggregateparameter order: corrected to match Laravel's($relations, $column, $function).- Removed global scopes propagated to aggregate queries:
withoutGlobalScope()on a parent query is now respected in aggregate sub-queries and miss-reload paths. flushModel()bypasses cooldown: manual flushes always invalidate immediately.- Scalar cache skips expression columns:
sum,avg,min,max,value, andpluckfall through to Eloquent forDB::raw()arguments. - Raw cache waiter takes over orphaned locks: a waiter that wakes to a dead builder now populates the cache itself.
- Query hit fast path: cached ID lists skip a round-trip when all model attributes are in cache.
- Lua scripts: overhauled for correctness and cluster compatibility.
- Cache keys: connection-aware keys prevent cross-connection collisions.
- Through relations: fixed cache invalidation gaps for
through-relation keys. - Transactions: invalidations inside a transaction are flushed atomically on commit.
- Model reloads:
fresh()/refresh()now bypasses cache, matching Laravel semantics. - Laravel compatibility: Laravel 11 / 12 fixes.
- Pivot and through relations: cache accuracy improvements.
- BelongsTo eager loads:
CacheableBelongsTowarmsbelongsToeager loads from the model cache. - Primary-key fast paths: optimized
whereInRaw,limit(0), and single-PK lookups. - Retrieved events:
NORMCACHE_FIRE_RETRIEVEDopts in to firing Eloquentretrievedon cache hits.
- Model hydration: rewritten for lower overhead.
- Invalidation cooldown: dropped invalidations are now lazily re-applied.
- Pivot constraint keys: pivot constraints are hashed into the key.
- Transaction invalidation: cache invalidations inside a transaction are deferred and applied atomically on commit.
- Initial release: first stable Normcache release.