4 ms·
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. He
by tored 3y ago
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));
}
- SavageBeast 3y ago"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." -- THATS THE TRUTH! Im still scarred from various production issues over the years where the cache keeps filling up all the memory and crashing for no discernable good reason. Yeah - theres a stack trace - it says that 3rd party rats nest library code I have no control over failed... Great, Thanks! ( Hey boss! I fixed that cache bug - I changed the 3rd party code in our dependency - now we're upgrade locked forever!!! Great right!! ) I do things like your example here when I know at what interval the underlying data is capable of changing. Batch process runs every night at midnight, no big deal, kill and rebuild the cache when it finishes. If you know when the data can change you can rebuild the cache on demand or let it gradually repopulate on its own nicely. The nasty part of caching is knowing when something changed in the underlying database such that the now invalidated cache entries can be evicted. Seems to me that when it absolutely needs to be up to the second kind of correct, we're best off skipping the "efficiency gain" a cache MAY offer in favor of direct SQL to an actual database connection. You can spend money on a BIG HOG of a database one time and know precisely how much you will spend to get the speed and reliability your use case demands. If you start trying to solve this with the caching/ORM route you're expense is NOT fixed. Dev Hours vs. Hardware Cost - Im buying hardware almost every time!
- tored 3y agoYes, the paradox with ORMs are that you actually need caching because they are typically very slow, with raw SQL and good indexes you can usually query database live for most of it. I find it usually better to have short timed cache in front of the renderer instead, like caching the HTML or JSON output of a view, then you don't end up with inconsistent data because one table was cached and another wasn't.