italix / cache
PSR-16 shaped caching: an opcache-backed file driver, an array driver, and an adapter over any Contracts KeyValueStore
Requires
- php: >=7.4
- italix/contracts: ^2.0
Requires (Dev)
- italix/crypto: ^2.0
- italix/testing: ^2.0
- psr/simple-cache: ^1.0 || ^2.0
Suggests
- ext-redis: Required by RedisCache; the driver to use when more than one machine serves the application
- ext-zend-opcache: FileCache is fastest when compiled entries stay in shared memory
- psr/simple-cache: ^1.0 || ^2.0 — lets Psr16Cache expose any driver here as a PSR-16 cache to other libraries
Provides
- psr/simple-cache-implementation: 1.0 || 2.0
This package is not auto-updated.
Last update: 2026-08-31 06:07:05 UTC
README
PSR-16 shaped caching with three drivers. One dependency: italix/contracts.
php src/Libs/Italix/Cache/tests/CacheTest.php # the drivers and the shared contract php src/Libs/Italix/Cache/tests/RedisCacheTest.php # the Redis driver php src/Libs/Italix/Cache/tests/Psr16CacheTest.php # conformance to PSR-16
Using it
$messages = $cache->remember('lang.it.messages', 3600, static function () use ($file): array { return require $file; });
remember() is the method that gets used; get/set/has/delete/clear are there for the
cases it does not fit.
A miss is never an error. Every read returns the default rather than throwing, because a cache
that can fail a request has made the application slower and less reliable. A corrupt FileCache
entry reads as a miss — there is a test for it.
The four drivers, and when each is right
| Speed | Shared between servers | Survives the request | Can hold a permanent entry | |
|---|---|---|---|---|
ArrayCache |
fastest | no | no | yes |
FileCache |
fast | no | yes | yes |
RedisCache |
a round trip | yes | yes | yes |
StoreCache |
a query | yes | yes | no |
FileCache writes entries as <?php return [...]; and reads them with include. On a server
with opcache that means a hit costs an opcode-cache lookup rather than a file read plus an
unserialize() — the value is already compiled, in shared memory, across requests.
The cost is what var_export() can round-trip: scalars, arrays, and objects with __set_state().
Not closures, not resources. That is checked at write time and refused, because storing it would
fatal on the next read instead of failing where the mistake is.
StoreCache wraps any Italix\Contracts\KeyValueStore, so the database-backed store
italix/crypto already ships becomes a cache shared between web heads with no new table. Slower by
a round trip, and correct where FileCache is not.
RedisCache is the one to reach for when more than one machine serves the application. Expiry is
the server's job rather than a column somebody has to sweep, and clear() actually works.
$redis = new \Redis(); $redis->connect('127.0.0.1', 6379); $cache = redis_cache($redis); // ext-redis, suggested not required
It implements Cache directly rather than arriving as a KeyValueStore through StoreCache. That
was the first plan, because it would have needed no new code — and it loses two capabilities Redis
has: StoreCache carries the limits of the database-backed store it was written for, which cannot
hold a permanent entry and cannot empty itself.
Three things about it are worth knowing before you use it:
- An unreachable Redis makes the application slow, not broken. Every call catches the connection
failing and behaves as a miss;
remember()still produces its value. Nothing in the response says the cache is down, so watch the server rather than the pages. - Objects are refused at write time, and any value that comes back containing one is treated as a
miss.
unserialize()on data an attacker can write is remote code execution, and a cache is a place things get written to — so reads passallowed_classes => false, and the suite proves it with a gadget class whose__wakeup()must never run. Scalars and arrays round-trip exactly, including integer keys and the difference between"1"and1, which JSON loses. clear()usesSCANover this cache's own prefix, neverFLUSHDB— which would sign out every session and reset every rate-limit counter sharing the instance — and neverKEYS, which blocks the server for as long as the keyspace takes to walk.
ArrayCache is worth having in front of any of them: a value read four times in one request should
be deserialised once. It counts hits and misses, so you can tell whether it was worth it.
What a TTL means
0 is no expiry. It is the default, because set($key, $value) with two arguments has to mean
something useful. A negative TTL is "already expired": the entry is removed and the call reports
success, because the post-condition the caller wants — this key does not resolve — has been reached.
Worth stating because it was not true. The interface said "0 or less means do not store" and no
driver did that: ArrayCache stored permanently for both, FileCache stored permanently for 0 and
deleted for negative, StoreCache threw for both. Three implementations, three answers, one sentence
of documentation, and nothing failing — because no assertion compared them. The conformance suite now
pins the behaviour for every driver in one section.
Two refusals
StoreCache::set() with a TTL of 0 throws. The underlying store has no permanent slot, and
storing something that vanishes at an hour nobody chose is worse than saying so.
StoreCache::clear() returns false. The store is shared with the rate limiter, and emptying it
would reset every counter along with the cache. FileCache::clear() is the one that works.
PSR-16, through an adapter
The method names and semantics are PSR-16's, in this namespace. To hand one of these caches to somebody else's library, wrap it:
$psr = new Psr16Cache(file_cache('/var/cache/app')); $router->setCache($psr); // expects Psr\SimpleCache\CacheInterface $psr->inner()->remember(...); // the methods PSR-16 has no word for
The package declares provide: psr/simple-cache-implementation, and keeps psr/simple-cache a
suggestion: the adapter is the only file that needs it, and pulling an interface into every install
for a class most consumers never construct decides on their behalf.
This README used to say adding implements to Cache itself would be "one line and no method
changes". Writing the adapter measured that claim and it was wrong by four: three bulk methods, a TTL
that is null|int|DateInterval, a reserved key charset ({}()/\@:) that must throw, and an
exception implementing PSR-16's own interface, because code written against the specification catches
Psr\SimpleCache\InvalidArgumentException and would not see anything else.
Written against psr/simple-cache ^1.0 || ^2.0. Version 3.0's signatures use mixed and union
types — PHP 8.0 — and this package supports 7.4; one class cannot implement both, because PHP matches
the signature against whichever interface is installed.
Deliberately not
No tag invalidation, no distributed lock, no cache-warming command.