Two layers sit in front of the database:
- An in-request memo — always on. It lives for one request or one queued job: it’s bound scoped in the container and also dropped when a queue job starts or Octane receives a request, so a long-running worker never keeps serving a value another process has since changed.
- A persistent cache — options.cache.enabled, on by default — in any Illuminate cache store. Writes and deletes update it once they commit, so every process sees a change from its next request or job on.
A read checks the memo, then the persistent store, then the database (falling back to default()). set() writes the new raw value to both layers, forget() puts an explicit “nothing stored” entry instead, and an import puts each imported raw value — there is nothing else to invalidate.
Raw values, never casts
Both layers hold the raw stored string, never the cast value: an encrypted option stays ciphertext in the cache, and nothing but scalars is serialized, so Laravel’s unserialize hardening (cache.serializable_classes) can’t turn a cached Collection or Carbon into __PHP_Incomplete_Class. Each read casts the raw value.
OPTIONS_CACHE_ENABLED=true
OPTIONS_CACHE_STORE=redis # null = the app’s default store
OPTIONS_CACHE_TTL=600 # seconds; null caches forever
OPTIONS_CACHE_PREFIX=options
OPTIONS_CACHE_TAG=optionsTransactions and concurrent reads
The memo takes the new value at once, so the writing request reads its own write — inside a transaction too, as its database connection would. The persistent entry is dropped at once and refilled only once the write commits: immediately outside a transaction, after the outermost commit inside one, never after a rollback. No other process is served a value the database never committed.
A read that misses fills the persistent entry with add() — only if nothing is cached yet. A write, forget or import that landed between that read and the fill has already put a newer entry, which the older read can’t overwrite, so the TTL never pins a stale value.
Flushing
Options::flushCache(); // clears the in-request memo and the persistent entriesphp artisan options:clear-cacheflushCache() and options:clear-cache work on every store. Each cache key embeds a generation token and a flush moves to a new one, so no old entry is read again — by other processes too, from their next request or job on. A taggable store (Redis, Memcached) also deletes the old entries at once; on any other store (file, database) they expire by cache.ttl, and with a null TTL they stay until the store evicts them.
Cache keys
Persistent keys follow {prefix}:{generation}:options:{key}:{scope}, where scope is global or an md5 of the length-prefixed owner morph class and owner key — so no two owners can share a key. The generation itself is stored under {prefix}:generation.
If the generation key goes missing — evicted, or after cache:clear — a fresh random token is minted with add() and read back, so requests that find it missing at the same time adopt one generation; an old generation is never reused. Writes go under the generation the store holds now, and also under the request’s memoised one when another process has moved on since the request began.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.