laminas/laminas-cache
Laminas Cache is a PHP caching component with PSR-6 and PSR-16 compliant adapters, multiple storage backends such as filesystem, memory, Redis, and more, plus cache patterns and integration for Laminas and Mezzio applications.
Install core + one adapter (adapters are split packages since 3.x):
composer require laminas/laminas-cache laminas/laminas-cache-storage-adapter-filesystem
(Swap in -redis, -memcached, -apcu, -blackhole, etc. as needed. Nothing is bundled by default — a missing adapter package is the #1 first-run error.)
Create the adapter through the factory, not new:
use Laminas\Cache\Service\StorageAdapterFactoryInterface;
$factory = app(StorageAdapterFactoryInterface::class);
$storage = $factory->create('filesystem', [
'cache_dir' => storage_path('framework/cache/laminas'),
'namespace' => 'myapp',
'ttl' => 3600,
]);
The standalone
new Filesystem([...])snippet that used to live in the package README was removed in 3.14.1 (#418, fixing #285) precisely because it no longer worked as written. Build adapters viaStorageAdapterFactoryInterface(or theStorageAdapterAbstractServiceFactoryin a PSR container) so options, plugin manager, and adapter resolution are wired correctly.
First use case — cache an expensive query result:
use Laminas\Cache\Storage\StorageInterface;
public function __construct(private StorageInterface $cache) {}
public function index()
{
$key = 'homepage.stats.v1';
if (! $this->cache->hasItem($key)) {
$this->cache->setItem($key, $this->computeStats(), 300);
}
return view('home', ['stats' => $this->cache->getItem($key)]);
}
PSR-16 (if you want the standard interface):
use Laminas\Cache\Psr\SimpleCache\SimpleCacheDecorator;
$psr16 = new SimpleCacheDecorator($storage);
$psr16->set('psr_key', ['a' => 1], 3600);
$psr16->get('psr_key', 'fallback');
Use CacheItemPoolDecorator for PSR-6.
// AppServiceProvider::register()
$this->app->singleton(StorageInterface::class, function ($app) {
return $app->make(StorageAdapterFactoryInterface::class)->create('filesystem', [
'cache_dir' => storage_path('framework/cache/laminas'),
'namespace' => config('app.name'),
]);
});
Type-hint StorageInterface (not a concrete adapter) so tests can swap in Laminas\Cache\Storage\Adapter\Memory or a BlackHole adapter.
Keeps Cache:: calls and Blade/queued-job code unchanged while backing them with Laminas:
// AppServiceProvider::boot()
Cache::extend('laminas', function ($app) {
$storage = $app->make(StorageInterface::class);
return Cache::repository(new class(new SimpleCacheDecorator($storage)) {
// adapt a PSR-16 store to Illuminate\Contracts\Cache\Store
});
});
Only worth it if you need an adapter Laravel doesn't ship (e.g. dba, mongodb via laminas-cache-storage-adapter-mongodb, session). Otherwise use Laravel's cache and skip the bridge entirely.
Keep key-building and TTL policy in one class instead of scattering md5() calls:
class LaminasCache
{
public function __construct(private StorageInterface $storage) {}
public function remember(string $bucket, string $id, int $ttl, callable $compute): mixed
{
$key = sprintf('%s:%s', $bucket, $id);
$value = $this->storage->getItem($key, $success);
if ($success) {
return $value;
}
$value = $compute();
$this->storage->setItem($key, $value, $ttl);
return $value;
}
}
getItem()'s by-ref $success flag is the reliable hit/miss signal — a cached false, 0, or empty array is otherwise indistinguishable from a miss.
Laminas has no tag API. Use a versioned namespace per bucket:
public function invalidateBucket(string $bucket): void
{
$this->storage->incrementItem("version:$bucket", 1); // or setItem to a random value
}
// key becomes "v{$version}:{$bucket}:{$id}"
Bumping the version orphans old entries; let the adapter's TTL/clearExpiredByFactor reclaim them.
Wire a hot APCu/Redis store and a filesystem fallback in a small composite, or simply prefer Redis in production and Memory in tests. Both are cheap to configure through the same factory:
$factory->create('redis', [
'host' => '127.0.0.1', 'port' => 6379,
'namespace' => 'myapp', 'ttl' => 3600,
]);
$items = $storage->getItems(['k1', 'k2', 'k3']); // returns found keys only
$storage->setItems($rows, 600); // one round trip
Prefer these over foreach when warming cache in an artisan command or queue worker.
Attach once, at construction, not per call:
$storage->addPlugin(new \Laminas\Cache\Storage\Plugin\Serializer());
$storage->addPlugin(new \Laminas\Cache\Storage\Plugin\ExceptionHandler());
ExceptionHandler converts adapter exceptions into false returns + logged throws — handy in request paths, dangerous if you rely on setItem() failures being fatal.
There is no setItem($k, $v, $ttl, ['tags']) and no clean(['tag']). The 4th parameter doesn't exist; clean() clears everything (or is not part of the storage API at all). Use prefixed/versioned keys (§4 above) or keep tag invalidation in Laravel's own cache.
new Filesystem(...) fatals if laminas-cache-storage-adapter-filesystem isn't installed. Same for every other adapter. The factory throws a clear "adapter not found" — always a composer problem, never a code problem.
Serializer plugin (or an adapter-level serializer option for Redis).Laminas\Cache\Exception\ExceptionInterface. Catch it at the boundary, or you'll 500 inside a request that thought it was just caching.Serializer twice double-encodes values and produces baffling "string instead of object" bugs.null = adapter default / no expiry; DateInterval = exact point in time. Fractional/floats get truncated.0 is not universally "forever" — some adapters treat it as an immediate miss. Use null for no-expiry.ClearExpiredByFactor/OptimizeByFactor if you write a lot of short-TTL keys.get($key, $default) takes a value, not a closure. No remember() in PSR-16 — implement it yourself.delete(), setMultiple(), clear() return bool; check them or you'll silently lose writes.set() of null is legal; distinguish "cached null" from "miss" via has() or the by-ref $success flag.3.14.1 replaced internal calls to methods deprecated in PHP 8.5 (#414). If you're on PHP 8.5, pin >=3.14.1 and re-run your test suite — otherwise expect deprecation noise (and future breakage) from laminas-cache internals under E_ALL.
try { } catch (\Laminas\Cache\Exception\ExceptionInterface $e) and log adapter name + key; core exceptions carry both.getMetadata($key) returns per-key metadata (ttl, mtime, filespec on filesystem) — useful for "why is this key gone?".setItem(); test the factory in tinker first.| Error | Cause | Fix |
|---|---|---|
Adapter … could not be found |
Adapter package not installed | composer require laminas/laminas-cache-storage-adapter-* |
Serialization of 'Closure' is not allowed |
Caching a closure/resource | Cache resolved values only; add Serializer for arrays/objects |
Cache directory … is not writable |
Filesystem perms | chmod -R 775 storage/framework/cache and check the namespace subdir |
Redis connection refused |
Bad host/port/auth | Verify phpredis/predis presence and host/port/password options |
| Stale data after "clearing" | Expected clean(['tag']) |
Use versioned key prefixes |
| Value comes back as a string | Double Serializer plugin |
Attach the plugin exactly once |
md5()/hash() long, user-controlled inputs to stay under adapter limits and avoid injection into keyspaces.namespace per app/environment so local, staging, and production sharing a Redis instance don't collide.How can I help you explore Laravel packages today?