PSR-6 w praktyce: Cache Interface w PHP
Standard PSR-6 definiuje wspólny interfejs do obsługi cache w aplikacjach PHP. Dzięki niemu możesz pisać kod niezależny od konkretnego mechanizmu przechowywania danych — bez względu na to, czy cache działa w oparciu o pliki, Redis, Memcached czy bazę danych.
Dlaczego PSR-6?
- Ujednolicony interfejs do zarządzania pamięcią podręczną,
- Łatwe podmienianie backendu cache bez zmian w logice aplikacji,
- Wsparcie dla bardziej rozbudowanych scenariuszy niż PSR-16 (Simple Cache),
- Kompatybilność z bibliotekami i frameworkami stosującymi standard PSR.
Kluczowe klasy w implementacji PSR-6
1. Cache
Klasa główna udostępniająca metody pracy z pamięcią podręczną — zgodnie z interfejsem
Psr\Cache\CacheItemPoolInterface
.
W implementacji możesz zastosować wzorzec
Adapter, aby obsłużyć różne typy backendów (np. Redis, pliki).
<?php
declare(strict_types=1);
namespace DJWeb\Framework\Cache;
use DateInterval;
use DJWeb\Framework\Config\Config;
use Psr\Cache\CacheItemPoolInterface;
class Cache
{
private static ?CacheItemPoolInterface $pool = null;
public static function init(CacheItemPoolInterface $pool): void
{
self::$pool = $pool;
}
public static function get(string $key, mixed $default = null): mixed
{
$item = self::$pool->getItem($key);
return $item->isHit() ? $item->get() : $default;
}
public static function put(string $key, mixed $value, DateInterval|int|null $ttl = null): bool
{
$item = self::$pool->getItem($key);
return self::$pool->save(
$item->set($value)->expiresAfter($ttl)
);
}
public static function forget(string $key): bool
{
Config::get('cache.default_driver');
return self::$pool->deleteItem($key);
}
public static function has(string $key): bool
{
return self::$pool->hasItem($key);
}
public static function remember(string $key, DateInterval|int $ttl, callable $callback): mixed
{
if (self::has($key)) {
return self::get($key);
}
$value = $callback();
self::put($key, $value, $ttl);
return $value;
}
}
2. CacheFactory
Fabryka odpowiedzialna za tworzenie instancji cache z odpowiednią konfiguracją. Użyty wzorzec Factory Method pozwala na elastyczne zarządzanie sposobem inicjalizacji i doborem backendu.
<?php
declare(strict_types=1);
namespace DJWeb\Framework\Cache;
use DJWeb\Framework\Cache\Storage\FileStorage;
use DJWeb\Framework\Cache\Storage\RedisStorage;
use DJWeb\Framework\Config\Config;
use InvalidArgumentException;
use Redis;
class CacheFactory
{
private static ?Redis $redis = null;
public static function withRedis(Redis $redis): void
{
self::$redis = $redis;
}
public static function create(): CacheItemPool
{
$driver = Config::get('cache.default_driver');
$config = Config::get("cache.stores.{$driver}");
$storage = match ($driver) {
'file' => self::createFileStorage($config),
'redis' => self::createRedisStorage($config),
default => throw new InvalidArgumentException("Unsupported cache driver: {$driver}")
};
return new CacheItemPool($storage);
}
private static function createFileStorage(array $config): FileStorage
{
$storage = new FileStorage($config['path']);
$storage->maxCapacity($config['max_items'] ?? 1000);
return $storage;
}
private static function createRedisStorage(array $config): RedisStorage
{
self::$redis ??= new Redis();
self::$redis->connect(
$config['host'] ?? 'localhost',
$config['port'] ?? 6379,
$config['timeout'] ?? 0.0,
);
if (isset($config['password'])) {
self::$redis->auth($config['password']);
}
if (isset($config['database'])) {
self::$redis->select($config['database']);
}
$storage = new RedisStorage(self::$redis, $config['prefix'] ?? 'cache:');
if (isset($config['max_memory'])) {
$storage->maxCapacity($config['max_memory']);
}
if (isset($config['eviction_policy'])) {
$storage->setEvictionPolicy($config['eviction_policy']);
}
return $storage;
}
}
3. CacheItem
Reprezentuje pojedynczy element cache. Przechowuje dane, czas życia (TTL) i status. W pełni zgodna implementacja PSR-6 wymagała tu kilku dodatkowych metod.
Można by ją znacznie uprościć korzystając z property hooks wprowadzonych w PHP 8.4, jednak w kursie zdecydowałem się tego nie robić, aby zachować pełną zgodność ze specyfikacją PSR.
Dodatkowo, od PHP 8.5 dzięki nowemu
mechanizmowi klonowania obiektów
klasa CacheItem
zostanie uproszczona. Aktualizacja kursu do PHP 8.5 planowana jest
na koniec roku.
<?php
declare(strict_types=1);
namespace DJWeb\Framework\Cache;
use Carbon\Carbon;
use DateInterval;
use Psr\Cache\CacheItemInterface;
class CacheItem implements CacheItemInterface
{
private mixed $value = null;
private bool $isHit = false;
private ?Carbon $expiry = null;
public function __construct(
private string $key,
) {
}
public function getKey(): string
{
return $this->key;
}
public function get(): mixed
{
return $this->value;
}
public function isHit(): bool
{
return $this->isHit;
}
public function set(mixed $value): static
{
return (clone $this)->with(value: $value);
}
public function expiresAt(?\DateTimeInterface $expiration): static
{
return (clone $this)->with(
expiry: $expiration ? Carbon::instance($expiration) : null
);
}
public function expiresAfter(\DateInterval|int|null $time): static
{
$expiry = match(true) {
$time === null => null,
$time instanceof DateInterval => Carbon::now()->add($time),
default => Carbon::now()->addSeconds($time),
};
return (clone $this)->with(expiry: $expiry);
}
public function withIsHit(bool $hit): static
{
return (clone $this)->with(isHit: $hit);
}
public function getExpiry(): ?Carbon
{
return $this->expiry;
}
private function with(
mixed $value = null,
?Carbon $expiry = null,
?bool $isHit = null,
): static {
$clone = clone $this;
$clone->value = $value ?? $this->value;
$clone->expiry = $expiry ?? $this->expiry;
$clone->isHit = $isHit ?? $this->isHit;
return $clone;
}
}
4. CacheItemPool
Kontener zarządzający zestawem obiektów CacheItem
. To tutaj odbywa się obsługa zapisu,
odczytu i usuwania wielu elementów naraz. Typowy przykład wzorca
Repository.
<?php
declare(strict_types=1);
namespace DJWeb\Framework\Cache;
use Carbon\Carbon;
use DJWeb\Framework\Cache\Contracts\StorageContract;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
class CacheItemPool implements CacheItemPoolInterface
{
private array $deferred = [];
public function __construct(
private readonly StorageContract $storage
) {
}
public function getItem(string $key): CacheItemInterface
{
if (isset($this->deferred[$key])) {
return $this->deferred[$key];
}
$item = new CacheItem($key);
if ($data = $this->storage->get($key)) {
if ($data['expiry'] === null || $data['expiry'] > Carbon::now()->getTimestamp()) {
return $item
->set($data['value'])
->withIsHit(true)
->expiresAt(
$data['expiry'] ? Carbon::createFromTimestamp($data['expiry']) : null
);
}
$this->storage->delete($key);
}
return $item;
}
public function getItems(array $keys = []): iterable
{
return array_combine(
$keys,
array_map(fn (string $key) => $this->getItem($key), $keys)
);
}
public function hasItem(string $key): bool
{
return $this->getItem($key)->isHit();
}
public function clear(): bool
{
$this->deferred = [];
return $this->storage->clear();
}
public function deleteItem(string $key): bool
{
unset($this->deferred[$key]);
return $this->storage->delete($key);
}
public function deleteItems(array $keys): bool
{
return ! in_array(
false,
array_map(fn ($key) => $this->deleteItem($key), $keys),
true
);
}
public function save(CacheItemInterface $item): bool
{
return $this->storage->set($item->getKey(), [
'value' => $item->get(),
'expiry' => $item->getExpiry()?->timestamp,
]);
}
public function saveDeferred(CacheItemInterface $item): bool
{
$this->deferred[$item->getKey()] = $item;
return true;
}
public function commit(): bool
{
if (array_any($this->deferred, fn ($item) => ! $this->save($item))) {
return false;
}
$this->deferred = [];
return true;
}
}
Definicje Storage — czyli kluczowego „mięska” całej implementacji — zostały szerzej omówione w kursie. Ich kod jest dostępny w repozytorium kursu na GitHubie.
Gdzie wykorzystasz PSR-6?
- Cache dla drogich zapytań do bazy danych,
- Przechowywanie wyników wywołań API,
- Optymalizacja dużych aplikacji webowych,
- Systemy kolejkowania i buforowania.
Podsumowanie
PSR-6 daje pełną kontrolę nad pamięcią podręczną i pozwala budować rozbudowane mechanizmy cache niezależnie od backendu. Dzięki niemu Twój kod staje się przenośny, elastyczny i zgodny z najlepszymi praktykami PHP-FIG.
📖 Oficjalna specyfikacja PSR-6: https://www.php-fig.org/psr/psr-6/
👉 To tylko fragment pełnego kursu MasterPHP!
W kursie znajdziesz kompletną implementację PSR-6 wraz z przykładami:
- tworzenie CacheItemPool od podstaw,
- implementacja różnych backendów Storage,
- zaawansowane strategie wygaszania i TTL,
- testy jednostkowe i integracyjne cache.