databases storage and caching
PSR-16 Simple Cache
The main skill is recognising when this compact interface is enough. PSR-16 is often easier to use than PSR-6 when application code needs straightforward cache reads and writes without cache-item objects or deferred saves.
The Basic Operations
The PSR-16 interface is Psr\SimpleCache\CacheInterface. Its core methods are:
get()andset();delete()andclear();getMultiple(),setMultiple(), anddeleteMultiple();has().
<?php
declare(strict_types=1);
function simpleCachePlan(string $operation, string $key): string
{
return match ($operation) {
'get' => 'get ' . $key,
'set' => 'set ' . $key . ' with a TTL',
'delete' => 'delete ' . $key,
default => 'choose a supported simple-cache operation',
};
}
echo simpleCachePlan('set', 'product.summary.42') . PHP_EOL;
// Prints:
// set product.summary.42 with a TTL
A package supplies the concrete implementation. Application code can depend on the interface while configuration chooses Redis, filesystem, memory, or another backend.
Read, Rebuild, Set
The usual flow is to ask the cache for a value, rebuild it on a miss, then store it with a TTL.
<?php
declare(strict_types=1);
function productSummaryCachePlan(bool $cacheHasValue): array
{
if ($cacheHasValue) {
return [
'source' => 'cache',
'action' => 'return the cached product summary',
];
}
return [
'source' => 'repository',
'action' => 'load the summary, set it with a TTL, and return it',
];
}
print_r(productSummaryCachePlan(false));
// Prints:
// [source] => repository
// [action] => load the summary, set it with a TTL, and return it
The source of truth remains outside the cache.
Defaults And Cached null
get($key, $default) returns the default when the key is missing. Be careful: if cached null is a valid value, a plain get() result cannot distinguish a cached null from a miss.
<?php
declare(strict_types=1);
function explainSimpleCacheLookup(mixed $value, bool $hasKey): string
{
if (!$hasKey) {
return 'cache miss';
}
return $value === null ? 'cache contains null' : 'cache contains a value';
}
echo explainSimpleCacheLookup(null, true) . PHP_EOL;
echo explainSimpleCacheLookup(null, false) . PHP_EOL;
// Prints:
// cache contains null
// cache miss
Use has() when this difference matters. In many applications, avoiding cached null values keeps the code simpler.
TTLs
set() accepts an optional TTL. PSR-16 supports an integer number of seconds or a DateInterval. A null TTL uses the implementation's default behaviour.
<?php
declare(strict_types=1);
function ttlForSimpleCache(string $valueType): int
{
return match ($valueType) {
'product_summary' => 300,
'shipping_quote' => 60,
'feature_flags' => 30,
default => 120,
};
}
echo ttlForSimpleCache('product_summary') . PHP_EOL;
// Prints:
// 300
Use a TTL that reflects how stale the value may safely become. Invalidate explicitly as well when important source data changes.
Multiple Keys
Batch operations are useful when loading several related values. They can reduce round trips to a remote cache backend.
<?php
declare(strict_types=1);
function productSummaryValues(array $productIds): array
{
$values = [];
foreach ($productIds as $productId) {
$values['product.summary.' . $productId] = [
'id' => $productId,
'cached' => true,
];
}
return $values;
}
print_r(productSummaryValues([42, 51]));
// Prints:
// [product.summary.42] => Array
// [product.summary.51] => Array
In a real cache implementation, pass that shape to setMultiple(). Use getMultiple() and deleteMultiple() when reading or invalidating a batch.
Key Rules
PSR-16 keys are strings. Implementations must support letters, numbers, underscore, and dot, and reserve {}, (), /, \, @, and :.
<?php
declare(strict_types=1);
function productSummaryKey(int $productId): string
{
return 'product.summary.' . $productId;
}
echo productSummaryKey(42) . PHP_EOL;
// Prints:
// product.summary.42
Build keys centrally so cache reads and deletes always agree.
Deleting And Clearing
Use delete() after the source value changes. Use clear() carefully because it removes unrelated values from the cache implementation.
<?php
declare(strict_types=1);
function deleteKeysAfterProductUpdate(int $productId): array
{
return [
'product.summary.' . $productId,
'product.detail.' . $productId,
'homepage.featured_products',
];
}
echo implode(', ', deleteKeysAfterProductUpdate(42)) . PHP_EOL;
// Prints:
// product.summary.42, product.detail.42, homepage.featured_products
Targeted invalidation protects both freshness and performance.
PSR-16 Compared With PSR-6
PSR-16 is deliberately small. It works directly with keys and values.
PSR-6 exposes cache items and pools. It has a separate hit check on each item, item expiry methods, and deferred saves followed by commit().
<?php
declare(strict_types=1);
function chooseCacheStandard(bool $needsDeferredWrites, bool $prefersDirectValues): string
{
if ($needsDeferredWrites) {
return 'PSR-6';
}
return $prefersDirectValues ? 'PSR-16' : 'either can fit';
}
echo chooseCacheStandard(false, true) . PHP_EOL;
// Prints:
// PSR-16
Neither standard decides whether the backend is Redis, filesystem, or something else. They define how PHP code talks to an implementation.
Failures And Exceptions
PSR-16 exceptions implement Psr\SimpleCache\CacheException. Invalid keys use Psr\SimpleCache\InvalidArgumentException.
Cache failure should normally lead to a safe fallback when the original data can be rebuilt.
<?php
declare(strict_types=1);
function cacheFailurePlan(bool $sourceCanBeReloaded): string
{
return $sourceCanBeReloaded
? 'log the cache problem and load from the source'
: 'surface the failure because no safe fallback exists';
}
echo cacheFailurePlan(true) . PHP_EOL;
// Prints:
// log the cache problem and load from the source
What To Check
Before moving on, make sure you can:
- describe the PSR-16 key/value API;
- rebuild and
set()a value after a miss; - use
has()when cachednullmust be distinguished from a miss; - set an appropriate TTL;
- use batch operations for several keys;
- build predictable PSR-16-safe keys;
- delete targeted keys after source data changes;
- explain the practical difference between PSR-16 and PSR-6.
Practice
Practice: Model A PSR-16 Product Cache
Build a small in-memory PHP model of a PSR-16-style cache for product summaries.
Requirements
- Implement
get(),set(),has(),delete(), andsetMultiple()methods. - Use a PSR-16-safe key such as
product.summary.42. - Rebuild and store a product summary after a miss.
- Set a TTL in seconds.
- Demonstrate a miss followed by a hit.
- Demonstrate targeted deletion and a multiple-value write.
You do not need to install a PSR-16 package. Model the small API so its purpose is clear.
Show solution
This small model keeps the direct key/value style visible.
<?php
declare(strict_types=1);
final class SimpleCache
{
/** @var array<string, mixed> */
private array $values = [];
/** @var array<string, int|null> */
private array $ttlSeconds = [];
public function get(string $key, mixed $default = null): mixed
{
return $this->values[$key] ?? $default;
}
public function set(string $key, mixed $value, ?int $ttl = null): void
{
$this->values[$key] = $value;
$this->ttlSeconds[$key] = $ttl;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->values);
}
public function delete(string $key): void
{
unset($this->values[$key], $this->ttlSeconds[$key]);
}
public function setMultiple(array $values, ?int $ttl = null): void
{
foreach ($values as $key => $value) {
$this->set((string) $key, $value, $ttl);
}
}
public function ttlFor(string $key): ?int
{
return $this->ttlSeconds[$key] ?? null;
}
}
function productSummary(SimpleCache $cache, int $productId): array
{
$key = 'product.summary.' . $productId;
$cached = $cache->get($key);
if (is_array($cached)) {
return $cached;
}
$fresh = [
'id' => $productId,
'name' => 'Desk lamp',
];
$cache->set($key, $fresh, 300);
return $fresh;
}
$cache = new SimpleCache();
echo $cache->has('product.summary.42') ? 'hit' : 'miss';
echo PHP_EOL;
productSummary($cache, 42);
echo $cache->has('product.summary.42') ? 'hit' : 'miss';
echo PHP_EOL;
echo $cache->ttlFor('product.summary.42') . PHP_EOL;
$cache->delete('product.summary.42');
echo $cache->has('product.summary.42') ? 'hit' : 'miss';
echo PHP_EOL;
$cache->setMultiple([
'product.summary.51' => ['id' => 51],
'product.summary.88' => ['id' => 88],
], 120);
echo $cache->has('product.summary.88') ? 'batch saved' : 'batch missing';
echo PHP_EOL;
// Prints:
// miss
// hit
// 300
// miss
// batch saved
In production, use a package that implements the real Psr\SimpleCache\CacheInterface. This model exists to show the smaller direct-value API.