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