I wouldn't pick an ORM even for caching, ORMs are typically a multi-layered blackbox magic based God class, thus very hard to debug when things hit the fan.
Here is I how I do it with simple SQL and built in PHP constructs, easy to read, debug and implement.
First part of the example is class based and the second one function based if you are more into that.
<?php
declare(strict_types=1);
// SQL with classes
namespace {
$pdo = new PDO('sqlite::memory:');
$pdo->exec(
"CREATE TABLE article (
article_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)
");
$pdo->exec("INSERT INTO article (name) VALUES('My Article')");
interface Cache
{
public function get(string $key): mixed;
public function set(string $key, mixed $data): void;
}
final class RuntimeCache implements Cache
{
private array $cache = [];
public function get(string $key): mixed
{
return $this->cache[$key] ?? null;
}
public function set(string $key, mixed $data): void
{
$this->cache[$key] = $data;
}
}
}
namespace Article {
use Cache;
use PDO;
readonly class Article
{
public int $article_id;
public string $name;
}
interface ArticleRepository
{
public function getArticleById(int $article_id): Article|null;
}
final class SqlArticleRepository implements ArticleRepository
{
public function __construct(private readonly PDO $pdo)
{
}
public function getArticleById(int $article_id): Article|null
{
$stmt = $this->pdo->prepare(
"SELECT article_id, name
FROM article
WHERE article_id = ?");
$stmt->execute([$article_id]);
$article = $stmt->fetchObject(Article::class);
return $article === false ? null : $article;
}
}
final class CachedArticleRepository implements ArticleRepository
{
public function __construct(private readonly Cache $cache,
private readonly ArticleRepository $articleRepository)
{
}
public function getArticleById(int $article_id): Article|null
{
$key = "article-{$article_id}";
$cached = $this->cache->get($key);
if (!empty($cached)) {
return $cached;
}
$article = $this->articleRepository->getArticleById($article_id);
if ($article !== null) {
$this->cache->set($key, $article);
}
return $article;
}
}
}
namespace {
$repo = new Article\CachedArticleRepository(
new RuntimeCache(),
new Article\SqlArticleRepository($pdo)
);
var_dump($repo->getArticleById(1));
var_dump($repo->getArticleById(1));
}
// SQL with functions
namespace {
function curry(callable $f, ...$args): callable {
$rf = new ReflectionFunction($f);
$count = $rf->getNumberOfParameters();
return function (...$arguments) use ($f, $rf, $count, $args) {
if (count($args) + count($arguments) >= $count) {
return $rf->invokeArgs(array_merge($args, $arguments));
}
return curry($f, ...array_merge($args, $arguments));
};
}
}
namespace Article\SqlRepository
{
use Article\Article;
function getArticleById(\PDO $pdo, int $article_id): Article|null
{
$stmt = $pdo->prepare(
"SELECT article_id, name
FROM article
WHERE article_id = ?");
$stmt->execute([$article_id]);
$article = $stmt->fetchObject(Article::class);
return $article === false ? null : $article;
}
}
namespace Article\CachedArticleRepository
{
use Article\Article;
function getArticleById(callable $cacheGet, callable $cacheSet, callable $getArticleById, int $article_id): Article|null
{
$key = "article-{$article_id}";
$cached = $cacheGet($key);
if (!empty($cached)) {
return $cached;
}
$article = $getArticleById($article_id);
if ($article !== null) {
$cacheSet($key, $article);
}
return $article;
}
}
namespace {
// for this example, just reuse existing class based implementation
$runtimeCache = new RuntimeCache();
$getArticleById = curry(Article\CachedArticleRepository\getArticleById(...),
$runtimeCache->get(...), $runtimeCache->set(...),
curry(Article\SqlRepository\getArticleById(...), $pdo));
var_dump($getArticleById(1));
var_dump($getArticleById(1));
}