italix/cache

PSR-16 shaped caching: an opcache-backed file driver, an array driver, and an adapter over any Contracts KeyValueStore

Maintainers

Package info

github.com/italix-net/cache

pkg:composer/italix/cache

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 0

1.3.1 2026-08-30 07:31 UTC

This package is not auto-updated.

Last update: 2026-08-31 06:07:05 UTC


README

PHP Version License

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 pass allowed_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" and 1, which JSON loses.
  • clear() uses SCAN over this cache's own prefix, never FLUSHDB — which would sign out every session and reset every rate-limit counter sharing the instance — and never KEYS, 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.