Ahnii!

Ever had your application slow to a crawl because of repeated database queries? Or struggled to switch between different caching libraries? Let’s dive into PSR-6, the standard that makes caching in PHP predictable and swappable!

This post is part of our PSR Standards in PHP series. If you’re new here, you might want to start with PSR-1 for the basics.

Prerequisites: PHP OOP (classes, interfaces). Recommended: Read PSR-4 first. See also: PSR-16 for simpler caching needs.

What Problem Does PSR-6 Solve? (2 minutes)

Before PSR-6, every caching library had its own way of doing things. Want to switch from Memcached to Redis? Rewrite your code. Moving from one framework to another? Learn a new caching API. PSR-6 fixes this by providing a common interface that all caching libraries can implement.

Core Interfaces (5 minutes)

Let’s look at the two main players:

1. CacheItemPoolInterface

This is your cache manager. Think of it as a warehouse where you store and retrieve items:

<?php

namespace Psr\Cache;

interface CacheItemPoolInterface
{
    public function getItem($key);
    public function getItems(array $keys = array());
    public function hasItem($key);
    public function clear();
    public function deleteItem($key);
    public function deleteItems(array $keys);
    public function save(CacheItemInterface $item);
    public function saveDeferred(CacheItemInterface $item);
    public function commit();
}

2. CacheItemInterface

This represents a single item in your cache:

<?php

namespace Psr\Cache;

interface CacheItemInterface
{
    public function getKey();
    public function get();
    public function isHit();
    public function set($value);
    public function expiresAt($expiration);
    public function expiresAfter($time);
}

Practical Usage (5 minutes)

Let’s see how to use this in real code:

<?php

// Basic usage
$pool = new FileCachePool('/path/to/cache');

try {
    // Store a value
    $item = $pool->getItem('user.1');
    if (!$item->isHit()) {
        $userData = $database->fetchUser(1); // Your database call
        $item->set($userData)
             ->expiresAfter(3600); // 1 hour
        $pool->save($item);
    }
    $user = $item->get();
} catch (\Exception $e) {
    // Handle errors gracefully
    log_error('Cache operation failed: ' . $e->getMessage());
    $user = $database->fetchUser(1); // Fallback to database
}

Common Pitfalls (3 minutes)

  1. Key Validation

    // Don't do this - using invalid characters
    $key = 'user@email.com';
    
    // Do this instead
    $key = 'user.' . md5('user@email.com');
    
  2. Error Handling

    // Always handle cache failures gracefully
    try {
        $pool->save($item);
    } catch (CacheException $e) {
        // Log and continue with a cache miss
    }
    
  3. Cache Stampede

    When a popular cache key expires, every request hits the database simultaneously:

    // Bad - All requests hit the database when cache expires
    $item = $pool->getItem('popular-posts');
    if (!$item->isHit()) {
        $data = $database->getPopularPosts(); // Hundreds of requests run this at once
        $item->set($data)->expiresAfter(60);
        $pool->save($item);
    }
    
    // Good - Stagger expiration with random jitter
    $item = $pool->getItem('popular-posts');
    if (!$item->isHit()) {
        $data = $database->getPopularPosts();
        $jitter = random_int(0, 30);
        $item->set($data)->expiresAfter(60 + $jitter);
        $pool->save($item);
    }
    

Framework Integration

Laravel

Laravel’s cache system supports PSR-6 through a bridge package:

<?php

use Illuminate\Support\Facades\Cache;

// Laravel's cache can be accessed as a PSR-6 pool
$pool = app('cache.psr6');
$item = $pool->getItem('user.1');

if (!$item->isHit()) {
    $item->set($user);
    $item->expiresAfter(3600);
    $pool->save($item);
}

Symfony

Symfony’s Cache component is a native PSR-6 implementation — no bridge needed:

<?php

use Symfony\Component\Cache\Adapter\FilesystemAdapter;

// Symfony's adapters implement PSR-6 directly
$cache = new FilesystemAdapter();
$item = $cache->getItem('user.1');

if (!$item->isHit()) {
    $item->set($user);
    $item->expiresAfter(3600);
    $cache->save($item);
}

What’s Next?

Tomorrow, we’ll look at PSR-7 (HTTP Message Interfaces). If you’re interested in simpler caching, stay tuned for our upcoming PSR-16 (Simple Cache) article, which offers a more straightforward alternative to PSR-6.

Try It Yourself

Clone the companion repository and explore the caching examples:

git clone https://github.com/jonesrussell/php-fig-guide.git
cd php-fig-guide
composer install
composer test -- --filter=PSR6

See src/Cache/ for the PSR-6 implementation used in the blog API.

Resources