marko/cache-redis
Redis cache driver --- fast, persistent caching backed by Redis for production workloads. Stores HMAC-signed, serialized data in Redis with automatic TTL expiration. Values are tamper-evident: reads verify the signature before deserializing, so modified entries are rejected loudly. Supports key prefixing to isolate cache namespaces. The Redis connection itself (host, port, database, password, TLS) comes from marko/redis, so the cache can share a server with other Redis-backed packages or get a connection of its own.
Implements CacheInterface from marko/cache.
Installation
Section titled “Installation”composer require marko/cache-redisThis automatically installs marko/cache, marko/redis (which brings predis/predis), and marko/encryption. A non-empty encryption.key is required; reads and writes throw Marko\Cache\Exceptions\TamperedCacheValueException if the key is empty or a stored value’s HMAC does not verify. Values are signed by the shared CacheValueSigner from marko/cache.
use Marko\Config\Env;
return [ 'key' => Env::string('APP_KEY', ''),];Configuration
Section titled “Configuration”Set the cache driver to redis in your config:
return [ 'driver' => 'redis', 'default_ttl' => 3600, 'path' => 'storage/cache',];The package ships config/cache-redis.php. It names the marko/redis connection to use and the key prefix; the connection settings themselves live in config/redis.php. No hand-written binding is needed:
use Marko\Config\Env;
return [ 'connection' => 'default', 'prefix' => Env::string('CACHE_PREFIX', 'marko:cache:'),];| Key | Env var | Default | Description |
|---|---|---|---|
connection | --- | default | A connection name from redis.connections in config/redis.php. A name that is not configured throws Marko\Redis\Exceptions\RedisConnectionException listing the configured names |
prefix | CACHE_PREFIX | marko:cache: | Prefix added to every cache key |
Set REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_DATABASE and REDIS_SCHEME to configure the default connection. See marko/redis for every connection key and for TLS.
A Separate Cache Database
Section titled “A Separate Cache Database”To keep the cache on its own database (what REDIS_CACHE_DATABASE used to do), add a cache connection and point the cache at it:
use Marko\Config\Env;
return [ 'connections' => [ 'cache' => [ 'host' => Env::string('REDIS_HOST', '127.0.0.1'), 'port' => Env::int('REDIS_PORT', 6379, min: 1, max: 65535), 'password' => Env::nullableString('REDIS_PASSWORD'), 'database' => Env::int('REDIS_CACHE_DATABASE', 1, min: 0), 'scheme' => Env::string('REDIS_SCHEME', 'tcp'), ], ],];use Marko\Config\Env;
return [ 'connection' => 'cache', 'prefix' => Env::string('CACHE_PREFIX', 'marko:cache:'),];The connection opens on first use. If Redis refuses it, a RedisConnectionException names the host and port and points to config/redis.php.
Upgrading from 0.11
Section titled “Upgrading from 0.11”Marko\Cache\Redis\RedisConnection and Marko\Cache\Redis\Exceptions\RedisConnectionException are gone; use Marko\Redis\RedisConnectionFactoryInterface and Marko\Redis\Exceptions\RedisConnectionException from marko/redis. The host, port, password, database and scheme keys moved from config/cache-redis.php to redis.connections.{name} in config/redis.php, and REDIS_CACHE_DATABASE is no longer read: the default connection uses REDIS_DATABASE. Use a cache connection, as above, to keep a separate database. RedisCacheDriver’s constructor now takes a RedisConnectionFactoryInterface and a RedisCacheConfig instead of a RedisConnection.
Once configured, inject CacheInterface as usual --- the Redis driver is used automatically:
use Marko\Cache\Contracts\CacheInterface;
class SessionStore{ public function __construct( private CacheInterface $cache, ) {}
public function getSession( string $token, ): ?array { return $this->cache->get("session.$token"); }
public function saveSession( string $token, array $data, ): void { $this->cache->set("session.$token", $data, ttl: 1800); }}When to Use
Section titled “When to Use”- Production workloads with high read/write throughput
- Multi-server deployments where cache must be shared
- Session storage and other latency-sensitive data
- TTL-managed expiration handled natively by Redis
Key Prefixing
Section titled “Key Prefixing”All keys are automatically prefixed (default: marko:cache:) to prevent collisions with other Redis data. Change it with the prefix key in config/cache-redis.php (or CACHE_PREFIX).
Atomic Locks
Section titled “Atomic Locks”The package also binds LockStoreInterface to Marko\Cache\Redis\Lock\RedisLockStore, so atomic locks work through LockFactoryInterface with no extra setup. Its scope is LockScope::Distributed. It is the only bundled lock store that excludes processes across hosts: every worker that shares the Redis server sees the same lock.
acquire()sends oneSET key owner NX PX ms, so of any number of clients exactly one gets the lock, and Redis expires it on time.release()runs a Lua compare-and-delete (EVAL): the key is deleted only when it still holds this owner’s token, so a holder whose lock expired can’t free the next owner’s lock.- Lock keys are the cache
prefixpluslock:(for examplemarko:cache:lock:video-pipeline:42), on the connection named bycache-redis.connection. They never collide with cache entries.clear()removes them along with the cache, because they share the prefix.
API Reference
Section titled “API Reference”Implements all methods from CacheInterface. See marko/cache for the full contract.
Key Methods
Section titled “Key Methods”| Method | Description |
|---|---|
get(string $key, mixed $default = null): mixed | Retrieve a value, returning $default on miss or expiration |
set(string $key, mixed $value, ?int $ttl = null): bool | Store a value with optional TTL (falls back to default_ttl) |
has(string $key): bool | Check if a non-expired entry exists |
delete(string $key): bool | Remove a single entry |
clear(): bool | Remove all prefixed entries from Redis |
getItem(string $key): CacheItemInterface | Get a CacheItem with hit/miss status and expiration metadata |
getMultiple(array $keys, mixed $default = null): iterable | Retrieve multiple values at once |
setMultiple(array $values, ?int $ttl = null): bool | Store multiple key-value pairs at once |
deleteMultiple(array $keys): bool | Remove multiple entries at once |
increment(string $key, int $ttl): int | Atomically increment an integer counter and apply its TTL in one step; an existing TTL is never reset |
add(string $key, mixed $value, ?int $ttl = null): bool | Atomically store a value only if the key is absent (SET ... NX); false when it exists |
remember(string $key, ?int $ttl, callable $callback): mixed | Return the cached value, or compute and store it on a miss (delegates to CacheOperations) |
pull(string $key, mixed $default = null): mixed | Get a value and delete it (delegates to CacheOperations) |
forever(string $key, mixed $value): bool | Store a value with no expiry (delegates to CacheOperations) |
RedisCacheConfig
Section titled “RedisCacheConfig”| Method | Description |
|---|---|
connection(): string | The marko/redis connection name from cache-redis.connection |
prefix(): string | The key prefix from cache-redis.prefix |
RedisCacheDriver and RedisLockStore each resolve the connection once, in their constructors, through RedisConnectionFactoryInterface::connection(). For the connection API see marko/redis.
Storage Details
Section titled “Storage Details”- Values are serialized with PHP’s
serialize(), wrapped in an HMAC-SHA256 envelope, and stored as Redis strings. Reads verify the HMAC before deserializing; tampered or corrupted entries throwTamperedCacheValueException. - The HMAC covers the prefixed Redis key as well as the value, so someone with write access to Redis can’t move a validly signed value from one key to another: the moved value throws
TamperedCacheValueException. Values signed by a release before key binding and HKDF subkeys throw too, so runmarko cache:clearwhen you deploy that upgrade. - A
nullTTL falls back todefault_ttlfrom config. A TTL greater than0uses RedisSETEXfor native expiration. A TTL of0or less means the entry never expires. add()sends oneSET key envelope NX EX ttl(noEXfor a TTL of0), writing the same signed envelope asset()under the prefixed key. Redis does the check and the write atomically, so of any number of clients exactly one getstrue.increment()runsINCRandEXPIREtogether in one Lua script (EVAL), so a crash can never leave a counter without an expiry. The TTL is set when the key is created (count reaches1), and is restored if the key somehow has none. Subsequent increments do not reset it, so the window stays fixed.- Counters are plain integers, not signed envelopes, because Redis writes them itself.
get(),getItem()andgetMultiple()return them asint, which is whatmarko/ratelimiterrelies on. Only values that are entirely an optional-followed by digits are read this way. They are never passed tounserialize(), and every other value must still carry a valid HMAC. clear()removes only keys matching the configured prefix --- other Redis data is not affected. It walks the keyspace withSCANin batches rather thanKEYS, so clearing a large database does not block Redis. Glob characters in the prefix are escaped, so the prefix always matches literally.getItem()reportsexpiresAt()as the current time from the PSR-20ClockInterface(marko/clock) plus the key’s remaining Redis TTL. Redis enforces the expiry itself, so moving aFakeClockchanges the reported time but never expires a key. Usemarko/cache-arrayto test expiry with a fake clock.
Related Packages
Section titled “Related Packages”marko/redis--- the shared Redis connections this driver usesmarko/cache--- the cache contracts this driver implements