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; } }