<?php
declare(strict_types=1);
namespace PhpMyAdmin\MoTranslator\Cache;
use PhpMyAdmin\MoTranslator\CacheException;
use PhpMyAdmin\MoTranslator\MoParser;
use function apcu_enabled;
use function apcu_entry;
use function apcu_exists;
use function apcu_fetch;
use function apcu_store;
use function array_combine;
use function array_keys;
use function array_map;
use function assert;
use function function_exists;
use function is_array;
use function is_string;
final class ApcuCache implements CacheInterface
{
public const LOADED_KEY = '__TRANSLATIONS_LOADED__';
/** @var MoParser */
private $parser;
/** @var string */
private $locale;
/** @var string */
private $domain;
/** @var int */
private $ttl;
/** @var bool */
private $reloadOnMiss;
/** @var string */
private $prefix;
public function __construct(
MoParser $parser,
string $locale,
string $domain,
int $ttl = 0,
bool $reloadOnMiss = true,
string $prefix = 'mo_'
) {
if (! (function_exists('apcu_enabled') && apcu_enabled())) {
throw new CacheException('ACPu extension must be installed and enabled');
}
$this->parser = $parser;
$this->locale = $locale;
$this->domain = $domain;
$this->ttl = $ttl;
$this->reloadOnMiss = $reloadOnMiss;
$this->prefix = $prefix;
$this->ensureTranslationsLoaded();
}
public function get(string $msgid): string
{
$msgstr = apcu_fetch($this->getKey($msgid), $success);
if ($success && is_string($msgstr)) {
return $msgstr;
}
if (! $this->reloadOnMiss) {
return $msgid;
}
return $this->reloadOnMiss($msgid);
}
private function reloadOnMiss(string $msgid): string
{
// store original if translation is not present
$cached = apcu_entry($this->getKey($msgid), static function () use ($msgid) {
return $msgid;
}, $this->ttl);
// if another process has updated cache, return early
if ($cached !== $msgid && is_string($cached)) {
return $cached;
}
// reload .mo file, in case entry has been evicted
$this->parser->parseIntoCache($this);
$msgstr = apcu_fetch($this->getKey($msgid), $success);
return $success && is_string($msgstr) ? $msgstr : $msgid;
}
public function set(string $msgid, string $msgstr): void
{
apcu_store($this->getKey($msgid), $msgstr, $this->ttl);
}
public function has(string $msgid): bool
{
return apcu_exists($this->getKey($msgid));
}
public function setAll(array $translations): void
{
$keys = array_map(function (string $msgid): string {
return $this->getKey($msgid);
}, array_keys($translations));
$translations = array_combine($keys, $translations);
assert(is_array($translations));
apcu_store($translations, null, $this->ttl);
}
private function getKey(string $msgid): string
{
return $this->prefix . $this->locale . '.' . $this->domain . '.' . $msgid;
}
private function ensureTranslationsLoaded(): void
{
// Try to prevent cache slam if multiple processes are trying to load translations. There is still a race
// between the exists check and creating the entry, but at least it's small
$key = $this->getKey(self::LOADED_KEY);
$loaded = apcu_exists($key) || apcu_entry($key, static function (): int {
return 0;
}, $this->ttl);
if ($loaded) {
return;
}
$this->parser->parseIntoCache($this);
apcu_store($this->getKey(self::LOADED_KEY), 1, $this->ttl);
}
}