Memoizer and Keys
Memoizer: Compute-and-Cache Without Reinventing Stampede Protection
use Derafu\Cache\Memoizer;
$memoizer = new Memoizer();
$value = $memoizer->remember($pool, 'some_key', function () {
return expensive_computation();
}, ttl: 3600);
Every hand-written PSR-6 caching decorator ends up rewriting the same “is it there? no? compute it, store it, return it” dance — getItem() → isHit() → set() → save(). That naive version has no protection against a cache stampede: under concurrent access, every process that hits a cold cache computes and saves independently.
Every symfony/cache adapter also implements Symfony’s own CacheInterface::get() contract, which does the same thing with real locking — only one process computes, the rest wait for it and read the result. Memoizer::remember() uses that path automatically whenever the given pool supports it, and falls back to the naive dance only for a PSR-6 implementation that isn’t a Symfony adapter (no locking possible without Symfony’s own contract). $ttl means exactly what it means for expiresAfter() — null leaves the pool’s default lifetime in effect, 0 means the entry never expires, a positive integer is seconds until expiration. Nothing here redefines that; a decorator that also needs a “don’t even try to cache” switch should express it as its own separately-named parameter, or — more simply — accept a LocalCacheBackend::None pool.
CacheKey: One Safe Way to Build a Key
use Derafu\Cache\CacheKey;
$cacheKey = new CacheKey();
$key = $cacheKey->build('my_package', 'ExampleWorker', ['api_resource' => true]);
// "my_package.ExampleWorker.e381541fbf7da0e4036f1cc881592fce"
PSR-6 forbids {}()/\@: in keys but sanitizes nothing for you — building a safe, collision-resistant key from real context (a class name with backslashes, a filter array) is left entirely to each caller. CacheKey::build() normalizes each part: a string gets its forbidden characters replaced (never dropped — dropping them could collide App\FooBar with App\Foo\Bar, both becoming AppFooBar), any other scalar is used as-is, and anything else (an array, an object) is hashed. If the assembled key would exceed 250 bytes (Memcached’s real limit, a safe ceiling for any backend), the whole thing collapses to the prefix plus a hash of the full key — long context never silently breaks a backend with an actual key-length limit.
CacheKey implements Derafu\Cache\Contract\CacheKeyInterface so a consumer that needs key logic driven by what’s actually being cached — not just its class name — can inject its own implementation of that one-method interface instead of subclassing CacheKey and reaching into its private sanitizing helpers.
CacheDirectory: Inspecting and Clearing by Path Alone
use Derafu\Cache\CacheDirectory;
$directory = new CacheDirectory('/var/cache/my_package');
$directory->stats(); // ['count' => 42, 'totalSize' => 1048576, 'oldest' => 1700000000, 'newest' => 1700003600]
$directory->clear(); // int — how many files were deleted.
stats() is a real, new capability — neither PSR-6 nor symfony/cache expose a way to ask any pool how many entries it holds or how much space they use, for any backend. clear() is not: CacheItemPoolInterface::clear() is already part of PSR-6 and works fine when you have a live pool instance in hand. CacheDirectory::clear() exists for exactly the case where you don’t — an admin command that only knows a path, not which adapter type (namespace, TTL, backend) originally wrote there.