361 lines
10 KiB
PHP
361 lines
10 KiB
PHP
<?php
|
|
namespace nulib\cache;
|
|
|
|
use Exception;
|
|
use nulib\cv;
|
|
use nulib\ext\utils;
|
|
use nulib\file;
|
|
use nulib\file\SharedFile;
|
|
use nulib\os\path;
|
|
use nulib\php\func;
|
|
use nulib\php\time\DateTime;
|
|
use nulib\php\time\Delay;
|
|
use nulib\str;
|
|
use nulib\ValueException;
|
|
|
|
class CacheFile extends SharedFile {
|
|
/** @var string|int durée de vie par défaut des données mises en cache */
|
|
const DURATION = "1D"; // jusqu'au lendemain
|
|
|
|
static function with($data, ?string $file=null): self {
|
|
if ($data instanceof self) return $data;
|
|
else return new static($file, $data);
|
|
}
|
|
|
|
protected static function ensure_source($data, ?CacheData &$source, bool $allowArray=true): bool {
|
|
if ($data === null || $data instanceof CacheData) {
|
|
$source = $data;
|
|
} elseif (is_subclass_of($data, CacheData::class)) {
|
|
$source = new $data();
|
|
} elseif (func::is_callable($data)) {
|
|
$source = new DataCacheData(null, $data);
|
|
} elseif (is_array($data) && $allowArray) {
|
|
return false;
|
|
} elseif (is_iterable($data)) {
|
|
$source = new DataCacheData(null, static function() use ($data) {
|
|
yield from $data;
|
|
});
|
|
} else {
|
|
throw ValueException::invalid_type($source, CacheData::class);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function __construct(?string $file, $data=null, ?array $params=null) {
|
|
$file ??= path::join(sys_get_temp_dir(), utils::uuidgen());
|
|
$file = path::ensure_ext($file, cache::EXT);
|
|
$basefile = str::without_suffix(cache::EXT, $file);
|
|
|
|
$this->initialDuration = Delay::with($params["duration"] ?? static::DURATION);
|
|
$this->overrideDuration = $params["override_duration"] ?? false;
|
|
$this->readonly = $params["readonly"] ?? false;
|
|
$this->cacheNull = $params["cache_null"] ?? false;
|
|
$data ??= $params["data"] ?? null;
|
|
$this->sources = null;
|
|
if (self::ensure_source($data, $source)) {
|
|
if ($source !== null) $source->setDatafile($basefile);
|
|
$this->sources = ["" => $source];
|
|
} else {
|
|
$sources = [];
|
|
$index = 0;
|
|
foreach ($data as $key => $source) {
|
|
self::ensure_source($source, $source, false);
|
|
if ($source !== null) {
|
|
$source->setDatafile($basefile);
|
|
if ($key === $index) {
|
|
$index++;
|
|
$key = $source->getName();
|
|
}
|
|
} elseif ($key === $index) {
|
|
$index++;
|
|
}
|
|
$sources[$key] = $source;
|
|
}
|
|
$this->sources = $sources;
|
|
}
|
|
parent::__construct($file);
|
|
}
|
|
|
|
protected Delay $initialDuration;
|
|
|
|
protected bool $overrideDuration;
|
|
|
|
protected bool $readonly;
|
|
|
|
protected bool $cacheNull;
|
|
|
|
/** @var ?CacheData[] */
|
|
protected ?array $sources;
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function isValid(): bool {
|
|
# considèrer que le fichier est invalide s'il est de taille nulle
|
|
return $this->getSize() > 0;
|
|
}
|
|
|
|
protected ?DateTime $start;
|
|
|
|
protected ?Delay $duration;
|
|
|
|
protected $data;
|
|
|
|
/** charger les données. le fichier a déjà été verrouillé en lecture */
|
|
protected function loadMetadata(): void {
|
|
if ($this->isValid()) {
|
|
$this->rewind();
|
|
[
|
|
"start" => $start,
|
|
"duration" => $duration,
|
|
"data" => $data,
|
|
] = $this->unserialize(null, false, true);
|
|
if ($this->overrideDuration) {
|
|
$duration = Delay::with($this->initialDuration, $start);
|
|
}
|
|
} else {
|
|
$start = null;
|
|
$duration = null;
|
|
$data = null;
|
|
}
|
|
$this->start = $start;
|
|
$this->duration = $duration;
|
|
$this->data = $data;
|
|
}
|
|
|
|
/**
|
|
* tester s'il faut mettre les données à jour. le fichier a déjà été
|
|
* verrouillé en lecture
|
|
*/
|
|
protected function shouldUpdate(bool $noCache=false): bool {
|
|
if ($this->isValid()) {
|
|
$expired = $this->duration->isElapsed();
|
|
} else {
|
|
$expired = false;
|
|
$noCache = true;
|
|
}
|
|
return $noCache || $expired;
|
|
}
|
|
|
|
/** sauvegarder les données. le fichier a déjà été verrouillé en écriture */
|
|
protected function saveMetadata(): void {
|
|
$this->duration ??= $this->initialDuration;
|
|
if ($this->start === null) {
|
|
$this->start = new DateTime();
|
|
$this->duration = Delay::with($this->duration, $this->start);
|
|
}
|
|
$this->ftruncate();
|
|
$this->serialize([
|
|
"start" => $this->start,
|
|
"duration" => $this->duration,
|
|
"data" => $this->data,
|
|
], false, true);
|
|
}
|
|
|
|
protected function unlinkFiles(bool $datafilesOnly=false): void {
|
|
foreach ($this->sources as $source) {
|
|
if ($source !== null) $source->delete();
|
|
}
|
|
if (!$datafilesOnly) @unlink($this->file);
|
|
}
|
|
|
|
/** tester si $value peut être mis en cache */
|
|
protected function shouldCache($value): bool {
|
|
return $this->cacheNull || $value !== null;
|
|
}
|
|
|
|
protected ?DateTime $ostart;
|
|
|
|
protected ?Delay $oduration;
|
|
|
|
protected $odata;
|
|
|
|
protected function beforeAction() {
|
|
$this->loadMetadata();
|
|
$this->ostart = cv::clone($this->start);
|
|
$this->oduration = cv::clone($this->duration);
|
|
$this->odata = cv::clone($this->data);
|
|
}
|
|
|
|
protected function afterAction() {
|
|
$modified = false;
|
|
if ($this->start != $this->ostart) $modified = true;
|
|
$duration = $this->duration;
|
|
$oduration = $this->oduration;
|
|
if ($duration === null || $oduration === null) $modified = true;
|
|
elseif ($duration->getDest() != $oduration->getDest()) $modified = true;
|
|
# égalité stricte uniquement pour $data et $datafiles
|
|
if ($this->data !== $this->odata) $modified = true;
|
|
if ($modified && !$this->readonly) {
|
|
$this->lockWrite();
|
|
$this->saveMetadata();
|
|
}
|
|
}
|
|
|
|
protected function action(callable $callback, bool $willWrite=false) {
|
|
if ($willWrite && !$this->readonly) $this->lockWrite();
|
|
else $this->lockRead();
|
|
try {
|
|
$this->beforeAction();
|
|
$result = $callback();
|
|
$this->afterAction();
|
|
return $result;
|
|
} finally {
|
|
$this->ostart = null;
|
|
$this->oduration = null;
|
|
$this->odata = null;
|
|
$this->start = null;
|
|
$this->duration = null;
|
|
$this->data = null;
|
|
$this->unlock(true);
|
|
}
|
|
}
|
|
|
|
protected function compute() {
|
|
return null;
|
|
}
|
|
|
|
protected function refreshData($data, bool $noCache) {
|
|
$source = $this->sources[$data] ?? null;
|
|
$updateMetadata = $this->shouldUpdate($noCache);
|
|
if ($source === null) $updateData = $this->data === null;
|
|
else $updateData = !$source->exists();
|
|
if (!$this->readonly && ($updateMetadata || $updateData)) {
|
|
$this->lockWrite();
|
|
if ($updateMetadata) {
|
|
# il faut refaire tout le cache
|
|
$this->unlinkFiles(true);
|
|
$this->start = null;
|
|
$this->duration = null;
|
|
$this->data = null;
|
|
$updateData = true;
|
|
}
|
|
if ($source === null) {
|
|
if ($updateData) {
|
|
# calculer la valeur
|
|
try {
|
|
$data = $this->compute();
|
|
} catch (Exception $e) {
|
|
# le fichier n'est pas mis à jour, mais ce n'est pas gênant: lors
|
|
# des futurs appels, l'exception continuera d'être lancée ou la
|
|
# valeur sera finalement mise à jour
|
|
throw $e;
|
|
}
|
|
} else {
|
|
$data = $this->data;
|
|
}
|
|
if ($this->shouldCache($data)) $this->data = $data;
|
|
else $this->data = $data = null;
|
|
} else {
|
|
if ($updateData) {
|
|
# calculer la valeur
|
|
try {
|
|
$data = $source->compute();
|
|
} catch (Exception $e) {
|
|
# le fichier n'est pas mis à jour, mais ce n'est pas gênant: lors
|
|
# des futurs appels, l'exception continuera d'être lancée ou la
|
|
# valeur sera finalement mise à jour
|
|
throw $e;
|
|
}
|
|
} else {
|
|
$data = $source->load();
|
|
}
|
|
if ($this->shouldCache($data)) {
|
|
$data = $source->save($data);
|
|
} else {
|
|
# ne pas garder le fichier s'il ne faut pas mettre en cache
|
|
$source->delete();
|
|
$data = null;
|
|
}
|
|
}
|
|
} elseif ($source === null) {
|
|
$data = $this->data;
|
|
} elseif ($source->exists()) {
|
|
$data = $source->load();
|
|
} else {
|
|
$data = null;
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* s'assurer que le cache est à jour avec les données les plus récentes. si
|
|
* les données sont déjà présentes dans le cache et n'ont pas encore expirées
|
|
* cette méthode est un NOP
|
|
*/
|
|
function refresh(bool $noCache=false): self {
|
|
$this->action(function() use ($noCache) {
|
|
foreach (array_keys($this->sources) as $data) {
|
|
$this->refreshData($data, $noCache);
|
|
}
|
|
});
|
|
return $this;
|
|
}
|
|
|
|
function get($data=null, bool $noCache=false) {
|
|
return $this->action(function () use ($data, $noCache) {
|
|
return $this->refreshData($data, $noCache);
|
|
});
|
|
}
|
|
|
|
function all($data=null, bool $noCache=false): ?iterable {
|
|
$data = $this->get($data, $noCache);
|
|
if ($data !== null && !is_iterable($data)) $data = [$data];
|
|
return $data;
|
|
}
|
|
|
|
function delete($data=null): void {
|
|
$source = $this->sources[$data] ?? null;
|
|
if ($source !== null) $source->delete();
|
|
}
|
|
|
|
/** obtenir les informations sur le fichier */
|
|
function getInfos(): array {
|
|
return $this->action(function () {
|
|
if (!$this->isValid()) {
|
|
return ["valid" => false];
|
|
}
|
|
$start = $this->start;
|
|
$duration = $this->duration;
|
|
return [
|
|
"valid" => true,
|
|
"start" => $start,
|
|
"duration" => strval($duration),
|
|
"date_start" => $start->format(),
|
|
"date_end" => $duration->getDest()->format(),
|
|
];
|
|
});
|
|
}
|
|
|
|
const UPDATE_SUB = -1, UPDATE_SET = 0, UPDATE_ADD = 1;
|
|
|
|
/**
|
|
* mettre à jour la durée de validité du fichier
|
|
*
|
|
* XXX UPDATE_SET n'est pas implémenté
|
|
*/
|
|
function updateDuration($nduration, int $action=self::UPDATE_ADD): void {
|
|
if ($this->readonly) return;
|
|
$this->action(function () use ($nduration, $action) {
|
|
if (!$this->isValid()) return;
|
|
$duration = $this->duration;
|
|
if ($action < 0) $duration->subDuration($nduration);
|
|
elseif ($action > 0) $duration->addDuration($nduration);
|
|
}, true);
|
|
}
|
|
|
|
/** supprimer les fichiers s'ils ont expiré */
|
|
function deleteExpired(bool $force=false): bool {
|
|
if ($this->readonly) return false;
|
|
return $this->action(function () use ($force) {
|
|
if ($force || $this->shouldUpdate()) {
|
|
$this->unlinkFiles();
|
|
return true;
|
|
}
|
|
return false;
|
|
}, true);
|
|
}
|
|
}
|