basedir = path::dirname($file); $basename = path::filename($file); $this->basename = str::without_suffix(self::EXT, $basename); $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 ($data === null) { $this->source = null; } elseif ($data instanceof CacheData) { $this->source = $data; } elseif (func::is_callable($data)) { $this->source = new CacheData($data); } elseif (!is_array($data)) { $this->source = self::ensure_source($data); } else { $sources = []; $index = 0; foreach ($data as $key => $source) { $source = self::ensure_source($source); if ($key === $index) { $index++; $key = $source->getName(); } $sources[$key] = $source; } $this->sources = $sources; $this->source = null; } parent::__construct($file); } /** @var string répertoire de base des fichiers de cache */ protected string $basedir; /** @var string nom de base des fichiers de cache */ protected string $basename; protected Delay $initialDuration; protected bool $overrideDuration; protected bool $readonly; protected bool $cacheNull; protected ?array $sources; protected ?CacheData $source; protected function getSource($data): CacheData { if ($data === null) { return $this->source ??= new CacheData(function() { return $this->compute(); }); } $source = $data; if (is_string($source) || is_int($source)) { $source = $this->sources[$source] ?? null; if ($source === null) throw ValueException::invalid_key($data); } if ($source instanceof CacheData) return $source; throw ValueException::invalid_type($data, CacheData::class); } /** * 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 ?array $datafilenames; /** 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, "datafiles" => $datafilenames, ] = $this->unserialize(null, false, true); if ($this->overrideDuration) { $duration = Delay::with($this->initialDuration, $start); } $datafilenames = array_fill_keys($datafilenames, true); } else { $start = null; $duration = null; $datafilenames = []; } $this->start = $start; $this->duration = $duration; $this->datafilenames = $datafilenames; } /** * 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); } $datafilenames = array_keys($this->datafilenames); $this->ftruncate(); $this->serialize([ "start" => $this->start, "duration" => $this->duration, "datafiles" => $datafilenames, ], false, true); } protected function loadData(string $datafile) { return file::reader($datafile)->unserialize(); } protected function saveData(string $datafile, $data): void { file::writer($datafile)->serialize($data); } protected function unlinkDatafile(string $datafilename): void { $datafile = path::join($this->basedir, $datafilename); @unlink($datafile); unset($this->datafilenames[$datafilename]); } protected function unlinkFiles(bool $datafilesOnly=false): void { if (!$datafilesOnly) @unlink($this->file); foreach ($this->datafilenames as $datafilename) { $this->unlinkDatafile($datafilename); } } /** 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 ?array $odatafilenames; protected function beforeAction() { $this->loadMetadata(); $this->ostart = cv::clone($this->start); $this->oduration = cv::clone($this->duration); $this->odatafilenames = cv::clone($this->datafilenames); } protected function afterAction() { $start = $this->start; $duration = $this->duration; $oduration = $this->oduration; $datafilenames = $this->datafilenames; $modified = false; if ($start != $this->ostart) $modified = true; if ($duration === null || $oduration === null) $modified = true; elseif ($duration->getDest() != $oduration->getDest()) $modified = true; # égalité stricte uniquement pour $datafiles qui est un array if ($datafilenames !== $this->odatafilenames) $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->start = null; $this->duration = null; $this->datafilenames = null; $this->unlock(true); } } protected function compute() { return null; } function get($data=null, bool $noCache=false) { return $this->action(function () use ($data, $noCache) { $source = $this->getSource($data); $dataname = $source->getName(); $datafilename = ".{$this->basename}.{$dataname}".self::EXT; $datafile = path::join($this->basedir, $datafilename); $updateMetadata = $this->shouldUpdate($noCache); $updateData = !array_key_exists($datafilename, $this->datafilenames); if (!$this->readonly && ($updateMetadata || $updateData)) { $this->lockWrite(); if ($updateMetadata) { # il faut refaire tout le cache $this->unlinkFiles(true); $this->start = null; $this->duration = null; $updateData = true; } if ($updateData) { # calculer un fichier try { $data = $source->get(); } catch (Exception $e) { # ne pas garder le fichier en cas d'exception $this->unlinkDatafile($datafile); throw $e; } } elseif (file_exists($datafile)) { $data = $this->loadData($datafile); } else { $data = null; } if ($this->shouldCache($data)) { $this->saveData($datafile, $data); $this->datafilenames[$datafilename] = true; } else { # ne pas garder le fichier s'il ne faut pas mettre en cache $this->unlinkDatafile($datafile); } } elseif (file_exists($datafile)) { $data = $this->loadData($datafile); } else { $data = null; } return $data; }); } 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->getSource($data); $dataname = $source->getName(); $datafilename = ".{$this->basename}.{$dataname}".self::EXT; $this->unlinkDatafile($datafilename); } /** 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; $datafilenames = $this->datafilenames; $datafiles = []; foreach (array_keys($datafilenames) as $datafilename) { $datafile = path::join($this->basedir, $datafilename); $name = $datafilename; str::del_prefix($name, ".{$this->basename}."); str::del_suffix($name, self::EXT); if (file_exists($datafile)) { $size = filesize($datafile); } else { $size = null; } $datafiles[$name] = $size; } return [ "valid" => true, "start" => $start, "duration" => strval($duration), "date_start" => $start->format(), "date_end" => $duration->getDest()->format(), "datafiles" => $datafiles, ]; }); } 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); } }