Pop PHP
The Toolkit

Cache

Anything expensive enough to hurt when it runs twice belongs in a cache — a rendered fragment, an API response, an aggregate query. pop-cache puts one API in front of eight storage backends, so the call sites you write against a file cache on a laptop keep working against Redis in production. You pick the backend once, when you construct the object, and nothing downstream changes.

BASH
composer require popphp/pop-cache

Getting Started#

A cache is a Pop\Cache\Cache wrapped around an adapter. The adapter decides where items live and carries the global time-to-live, in whole seconds.

PHP
use Pop\Cache\Cache;
use Pop\Cache\Adapter\File;

$cache = new Cache(new File(__DIR__ . '/../cache', 300));

$cache->saveItem('user-1001', ['id' => 1001, 'name' => 'John Smith']);

$cache->getItem('user-1001');  // the array back
$cache->hasItem('user-1001');  // true
$cache->deleteItem('user-1001');

saveItem() takes an optional third argument to override the global TTL for one item. A TTL of 0 means the item never expires, which is also the default every adapter constructor uses when you omit it — pass a number of seconds unless you mean forever.

Create the File adapter's directory, writable, before constructing it — File calls setDir() from its constructor, so a missing or unwritable path throws Pop\Cache\Adapter\Exception at bootstrap rather than at the first write.

getItem() returns false for a miss, which is ambiguous the moment you cache a boolean. Pass a second argument and that value is returned on a genuine miss instead:

PHP
use Pop\Cache\Cache;
use Pop\Cache\Adapter\Memory;

$cache = new Cache(new Memory());

$cache->saveItem('feature-enabled', false);

$cache->getItem('feature-enabled', null); // false — the real cached value
$cache->getItem('never-cached', null);    // null — a genuine miss

Keys are validated. {, }, (, ), /, \, @ and : are the characters PSR-16 reserves, and an empty key is rejected under a rule of its own; all of them throw Pop\Cache\InvalidArgumentException on the way in with the same message. The colon is the one that catches people out, since user:1001 is the house style in other caching libraries — write user-1001 here.

Choosing an Adapter#

Every adapter implements Pop\Cache\Adapter\AdapterInterface, so swapping one for another is a one-line change at construction.

Adapter Constructor Reach for it when
File new File($dir, $ttl) A single server with a writable disk. The default choice.
Redis new Redis($ttl, $host, $port) Several web servers share one cache, and you want persistence.
Memcached new Memcached($ttl, $host, $port) Several web servers share one cache, purely volatile.
Apc new Apc($ttl) One server, hottest possible reads — APCu lives in the PHP process.
Database new Database($db, $ttl) A pop-db connection is all the shared storage you have.
Session new Session($ttl) Per-user data for the length of one visit.
Memory new Memory($ttl) Tests, and caching within a single request.
NullAdapter new NullAdapter() Turning caching off without touching a call site.

The Database adapter takes a Pop\Db\Adapter\AbstractAdapter as its first argument and creates its pop_cache table from its constructor, before anything is cached. Redis, Memcached, Apc and Session each accept a $namespace argument (default pop_cache), which is what keeps clear() from wiping a neighboring application sharing the same backend.

Two static methods report what the current runtime can support, which is how you pick without hardcoding an assumption about the deployment:

PHP
use Pop\Cache\Cache;
use Pop\Cache\Adapter\File;
use Pop\Cache\Adapter\Redis;

$cache = Cache::isAvailable('redis')
    ? new Cache(new Redis(300))
    : new Cache(new File(__DIR__ . '/../cache', 300));

Cache::getAvailableAdapters() returns the whole map, keyed apc, file, memcached, memory, null, redis, session and sqlite, with memory, null and file always true. The Database adapter is reported under sqlite, and that key answers a narrower question than it looks: the probe is class_exists('Sqlite3') or sqlite among the PDO drivers, so it says nothing at all about a MySQL- or PostgreSQL-backed Database adapter.

getAvailableAdapters() reports what the PHP runtime has loaded rather than whether a backing service answers, so confirm the connection itself before relying on Redis, Memcached or APCu.

Computing on a Miss#

remember() collapses the check-compute-store dance into one call. On a hit it returns the cached value; on a miss it runs the callback, caches what comes back, and returns it.

PHP
use Pop\Cache\Cache;
use Pop\Cache\Adapter\File;

$cache = new Cache(new File(__DIR__ . '/../cache', 300));

$report = $cache->remember('sales-report', function () {
    return ['total' => 4820]; // runs only on a miss
}, 300, 1.0);

A falsy return — false, null, 0, '' — is cached correctly and does not send the callback round again. If the callback throws, the exception propagates unchanged and nothing is written.

The fourth argument, $beta, defaults to 0.0 and turns on probabilistic early recomputation above that. As an item nears its TTL each read has a growing chance of refreshing it early, so a rebuild usually falls to one caller while everyone else is served the still-valid copy. 1.0 is a sensible starting point. It protects the steady state rather than a cold key, which still lets every concurrent caller compute at once.

Bulk Invalidation with Tags#

When one change invalidates a set of keys you cannot enumerate, tag them on the way in and invalidate the tag.

PHP
use Pop\Cache\Cache;
use Pop\Cache\Adapter\Memory;

$cache = new Cache(new Memory());

$cache->saveTaggedItem('product-1', 'widget', ['products', 'category-electronics']);
$cache->saveTaggedItem('product-2', 'novel', ['products', 'category-books']);

$cache->invalidateTag('category-electronics'); // deletes product-1, leaves product-2
$cache->invalidateTags(['products', 'category-books']);

Tag names go through the same reserved-character rule as keys, so category:electronics throws. Re-saving an id with a different tag list moves it correctly between tags.

Once a key is tagged, keep writing it through saveTaggedItem() so the tag index stays in step.

PSR-16 and PSR-6#

Pop\Cache\Cache implements Psr\SimpleCache\CacheInterface directly, so an instance you already have is a valid PSR-16 cache — get(), set(), has(), delete(), clear() and the *Multiple() methods sit alongside the *Item() names and read the same values.

PHP
use Pop\Cache\Cache;
use Pop\Cache\Adapter\File;
use Psr\SimpleCache\CacheInterface;

function warmUp(CacheInterface $cache): void
{
    $cache->set('greeting', 'Hello', 300);
}

warmUp(new Cache(new File(__DIR__ . '/../cache', 300)));

A TTL of 0 means "forever" to saveItem() and "delete" to set(), which is what PSR-16 mandates. Pick one family per call site.

PSR-6 cannot live on the same class, because it declares getItem() and friends with incompatible signatures, so it ships as Pop\Cache\Psr6\CacheItemPool wrapping the same adapters:

PHP
use Pop\Cache\Adapter\File;
use Pop\Cache\Psr6\CacheItemPool;

$pool = new CacheItemPool(new File(__DIR__ . '/../cache', 300));

$item = $pool->getItem('config-hash');

if (!$item->isHit()) {
    $item->set('abc123');
    $item->expiresAfter(300);
    $pool->save($item);
}

Build both around one adapter instance when an application needs the native API and a PSR-6 pool at once.

Adapter-specific behavior goes well past this page — the atomicity and TTL differences between counter implementations, what destroy() tears down on each backend, how cached objects survive serialization, and the clock you can inject for deterministic TTL tests — see the pop-cache README.

See Also#