PSR-6: Caching Interface
debt(d6/e5/b5/t5)
Closest to 'only careful code review' (d7), slightly better at d6 — phpstan/deptrac can detect direct Redis/APCu coupling via layer rules, but spotting missing isHit() checks or absent expiry usually requires review.
Closest to 'touches multiple files / significant refactor in one component' (e5) — swapping direct cache calls for CacheItemPoolInterface requires introducing the abstraction, updating call sites, and wiring DI across the caching component.
Closest to 'persistent productivity tax' (b5) — caching applies across web/cli/queue contexts and the interface choice shapes how every cache consumer is written, but it's a well-known standard so the tax is modest.
Closest to 'notable trap most devs eventually learn' (t5) — the misconception that PSR-6 and PSR-16 compete, plus the isHit() gotcha where missing items return null and look like cached nulls, are documented traps developers hit once.
Also Known As
TL;DR
Explanation
PSR-6 defines two interfaces: CacheItemPoolInterface (the cache driver — get, save, deleteItem, clear, commit) and CacheItemInterface (a cache entry — get/set value, expiry). By type-hinting CacheItemPoolInterface, application code is decoupled from the specific backend (Redis, Memcached, APCu, filesystem). Symfony Cache is the canonical implementation, supporting all backends with a unified API. PSR-16 (Simple Cache) provides a simpler single-class alternative for basic get/set/delete use cases that don't need the item object model. Libraries should accept PSR-6 or PSR-16 interfaces rather than concrete cache clients.
Common Misconception
Why It Matters
Common Mistakes
- Using cache library APIs directly instead of PSR-6 — locks the codebase to one cache implementation.
- Not checking CacheItem::isHit() — treating a miss as a hit returns stale null data.
- Not setting expiry on cache items — items live forever until manually deleted or evicted.
- Serializing objects manually before caching instead of letting PSR-6 handle serialization.
Code Examples
// Direct Redis dependency — cannot swap to file cache:
$redis = new Redis();
$redis->get('user:' . $id);
// PSR-6 — swappable:
function getUser(CacheItemPoolInterface $cache, int $id): User {
$item = $cache->getItem('user:' . $id);
if (!$item->isHit()) {
$item->set(User::find($id))->expiresAfter(300);
$cache->save($item);
}
return $item->get();
}
// PSR-6 Caching Interface
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\CacheItemInterface;
// Writing to cache
$item = $cache->getItem('user:' . $userId);
if (!$item->isHit()) {
$user = $this->db->fetchUser($userId);
$item->set($user)->expiresAfter(3600);
$cache->save($item);
} else {
$user = $item->get();
}
// Delete
$cache->deleteItem('user:' . $userId);
// PSR-6 compatible implementations:
// symfony/cache, cache/filesystem-adapter, league/flysystem-cached-adapter
// PSR-16 (Simple Cache) — simpler interface:
// $cache->get('key', 'default');
// $cache->set('key', $value, 3600);