300 lines
8.1 KiB
PHP
300 lines
8.1 KiB
PHP
<?php
|
|
namespace nur\b\io;
|
|
|
|
use ArrayAccess;
|
|
use Countable;
|
|
use Exception;
|
|
use Generator;
|
|
use nur\A;
|
|
use nur\b\coll\TBaseArray;
|
|
use nur\b\IllegalAccessException;
|
|
use nur\b\params\Parametrable;
|
|
use nur\b\params\Tparametrable;
|
|
use nur\data\types\Tmd;
|
|
use nur\file;
|
|
use nur\os;
|
|
use nur\sery\php\time\DateTime;
|
|
use nur\sery\php\time\Delay;
|
|
|
|
/**
|
|
* Class FileCachedValue: un fichier utilisé pour mettre en cache certaines
|
|
* données. les données sont (re)calculée si la durée de vie du cache est
|
|
* dépassée
|
|
*/
|
|
abstract class FileCachedValue extends Parametrable implements ArrayAccess, Countable {
|
|
use Tparametrable, TBaseArray, Tmd;
|
|
|
|
/** @var string chemin vers le fichier cache par défaut */
|
|
const FILE = null;
|
|
|
|
/** @var int durée de vie par défaut du cache */
|
|
const DURATION = "1D"; // jusqu'au lendemain
|
|
|
|
/** @var bool faut-il mettre en cache la valeur nulle? */
|
|
const CACHE_NULL = false;
|
|
|
|
/** @var array schéma des données supplémentaires */
|
|
const SCHEMA = null;
|
|
|
|
function __construct(?array $params=null) {
|
|
self::set_parametrable_params_defaults($params, [
|
|
"file" => static::FILE,
|
|
"duration" => static::DURATION,
|
|
"cache_null" => static::CACHE_NULL,
|
|
]);
|
|
[$params, $data] = $this->splitParametrableParams($params);
|
|
if ($data) A::merge($params["data"], $data);
|
|
parent::__construct($params);
|
|
}
|
|
|
|
const PARAMETRABLE_PARAMS_SCHEMA = [
|
|
"file" => ["?string", null, "chemin vers le fichier"],
|
|
"duration" => [Delay::class, null, "durée de vie du cache"],
|
|
"override_duration" => ["bool", false, "faut-il ignorer la duration inscrite dans le fichier et prendre la valeur locale?"],
|
|
"cache_null" => ["bool", false, "faut-il mettre en cache la valeur null?"],
|
|
"data" => ["array", null, "données supplémentaires"]
|
|
];
|
|
|
|
protected $ppFile;
|
|
|
|
function getFile(): string {
|
|
$cacheFile = $this->ppFile;
|
|
if ($cacheFile === null) {
|
|
$rand = bin2hex(random_bytes(8));
|
|
$this->ppFile = $cacheFile = sys_get_temp_dir()."/$rand.cache";
|
|
}
|
|
return $cacheFile;
|
|
}
|
|
|
|
/** @var Delay */
|
|
protected $ppDuration;
|
|
|
|
/** @var bool */
|
|
protected $ppOverrideDuration;
|
|
|
|
protected $ppCacheNull;
|
|
|
|
function pp_setData($data): void {
|
|
$md = $this->md();
|
|
if ($md !== null) $md->ensureSchema($data);
|
|
$this->data = $data;
|
|
}
|
|
|
|
/** calculer la donnée */
|
|
protected abstract function compute();
|
|
|
|
/** sérialiser la donnée avant de l'enregistrer */
|
|
protected function serialize($data): string {
|
|
return serialize($data);
|
|
}
|
|
|
|
/** enregistrer la donnée dans le fichier spécifié */
|
|
protected function save($data, string $file): void {
|
|
$outf = file::open($file, "wb");
|
|
try {
|
|
$contents = $this->serialize($data);
|
|
$tstart = new DateTime();
|
|
$duration = Delay::with($this->ppDuration, $tstart);
|
|
fwrite($outf, serialize([$tstart, $duration])."\n");
|
|
fwrite($outf, $contents);
|
|
} finally {
|
|
fclose($outf);
|
|
}
|
|
}
|
|
|
|
/** désérialiser la données depuis le contenu spécifié */
|
|
protected function unserialize(string $contents) {
|
|
return unserialize($contents);
|
|
}
|
|
|
|
/** charger la donnée depuis le fichier spécifié */
|
|
protected function load(string $file) {
|
|
$inf = file::open($file, "rb");
|
|
try {
|
|
fgets($inf); // sauter TSDATA
|
|
$contents = stream_get_contents($inf);
|
|
return $this->unserialize($contents);
|
|
} finally {
|
|
fclose($inf);
|
|
}
|
|
}
|
|
|
|
protected $fp;
|
|
|
|
protected function _open(): void {
|
|
$file = $this->getFile();
|
|
IOException::ensure_not_false(os::mkdirof($file));
|
|
$this->fp = IOException::ensure_not_false(fopen($file, "c+b"));
|
|
}
|
|
|
|
protected function _lock(int $operation): void {
|
|
if (!flock($this->fp, $operation)) {
|
|
switch ($operation) {
|
|
case LOCK_SH: $msg = "sh_lock"; break;
|
|
case LOCK_EX: $msg = "ex_lock"; break;
|
|
}
|
|
throw IllegalAccessException::unexpected_state($msg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* vérifier si le fichier est valide. s'il est invalide, il faut le recréer.
|
|
*
|
|
* on assume que le fichier existe, vu qu'il a été ouvert en c+b
|
|
*/
|
|
protected function isValidFile(): bool {
|
|
# on considère que le fichier est invalide s'il est de taille nulle
|
|
return filesize($this->getFile()) > 0;
|
|
}
|
|
|
|
protected function loadInfos(): array {
|
|
[$tstart, $duration] = unserialize(fgets($this->fp));
|
|
if (is_int($tstart)) {
|
|
$tstart = new DateTime($tstart);
|
|
$duration = new Delay($duration, $tstart);
|
|
}
|
|
if ($this->ppOverrideDuration) {
|
|
$duration = Delay::with($this->ppDuration, $tstart);
|
|
}
|
|
return [$tstart, $duration];
|
|
}
|
|
|
|
protected function shouldUpdate(bool $noCache=false) {
|
|
/** @var Delay $duration */
|
|
$this->_open();
|
|
$cleanup = true;
|
|
try {
|
|
$this->_lock(LOCK_SH);
|
|
if ($this->isValidFile()) {
|
|
[$tstart, $duration] = $this->loadInfos();
|
|
$expired = $duration->isElapsed();
|
|
} else {
|
|
$expired = false;
|
|
$noCache = true;
|
|
}
|
|
if ($noCache || $expired) {
|
|
$this->_lock(LOCK_EX);
|
|
$cleanup = false;
|
|
return true;
|
|
}
|
|
} finally {
|
|
if ($cleanup) $this->cleanup();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* indiquer si la valeur calculée doit être mise en cache. par défaut, la
|
|
* valeur nulle n'est pas mise en cache.
|
|
*/
|
|
protected function shouldCache($value): bool {
|
|
return $this->ppCacheNull || $value !== null;
|
|
}
|
|
|
|
protected function cleanup(): void {
|
|
if ($this->fp !== null) {
|
|
fclose($this->fp);
|
|
$this->fp = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* obtenir la donnée.
|
|
*
|
|
* si la donnée a déjà été mise en cache au préalable, et que la durée de
|
|
* validité du cache n'est pas dépassée, retourner la valeur mise en cache.
|
|
* sinon, (re)calculer la donnée
|
|
*
|
|
* @param bool $noCache ne pas utiliser le cache (forcer le recalcul)
|
|
*/
|
|
function get(bool $noCache=false) {
|
|
try {
|
|
if ($this->shouldUpdate($noCache)) {
|
|
try {
|
|
$data = $this->compute();
|
|
if ($data instanceof Generator) $data = iterator_to_array($data);
|
|
if ($this->shouldCache($data)) {
|
|
$this->save($data, $this->ppFile);
|
|
} else {
|
|
# ne pas garder le fichier s'il ne faut pas mettre en cache
|
|
unlink($this->ppFile);
|
|
}
|
|
} catch (Exception $e) {
|
|
// si une erreur se produit, ne pas garder le fichier
|
|
unlink($this->ppFile);
|
|
throw $e;
|
|
}
|
|
} else {
|
|
$data = $this->load($this->ppFile);
|
|
}
|
|
return $data;
|
|
} finally {
|
|
$this->cleanup();
|
|
}
|
|
}
|
|
|
|
function getInfos(): array {
|
|
$this->_open();
|
|
try {
|
|
$this->_lock(LOCK_SH);
|
|
if (!$this->isValidFile()) return ["valid" => false];
|
|
$size = filesize($this->ppFile);
|
|
/**
|
|
* @var DateTime $tstart
|
|
* @var Delay $duration
|
|
*/
|
|
[$tstart, $duration] = $this->loadInfos();
|
|
return [
|
|
"valid" => true,
|
|
"size" => $size,
|
|
"tstart" => $tstart,
|
|
"duration" => strval($duration),
|
|
"date_start" => $tstart->format(),
|
|
"date_end" => $duration->getDest()->format(),
|
|
];
|
|
} finally {
|
|
$this->cleanup();
|
|
}
|
|
}
|
|
|
|
const UPDATE_SUB = -1, UPDATE_SET = 0, UPDATE_ADD = 1;
|
|
|
|
/** mettre à jour la durée de validité du fichier */
|
|
function updateDuration($nduration, int $action=1): void {
|
|
$this->_open();
|
|
try {
|
|
$this->_lock(LOCK_SH);
|
|
if (!$this->isValidFile()) return;
|
|
$this->_lock(LOCK_EX);
|
|
$fp = $this->fp;
|
|
/**
|
|
* @var DateTime $tstart
|
|
* @var Delay $duration
|
|
*/
|
|
[$tstart, $duration] = $this->loadInfos();
|
|
$contents = stream_get_contents($fp);
|
|
if ($action < 0) $duration->subDuration($nduration);
|
|
elseif ($action > 0) $duration->addDuration($nduration);
|
|
fseek($fp, 0);
|
|
ftruncate($fp, 0);
|
|
fwrite($fp, serialize([$tstart, $duration])."\n");
|
|
fwrite($fp, $contents);
|
|
} finally {
|
|
$this->cleanup();
|
|
}
|
|
}
|
|
|
|
/** supprimer le fichier s'il a expiré */
|
|
function deleteExpired(): bool {
|
|
try {
|
|
if ($this->shouldUpdate()) {
|
|
unlink($this->ppFile);
|
|
return true;
|
|
}
|
|
} finally {
|
|
$this->cleanup();
|
|
}
|
|
return false;
|
|
}
|
|
}
|