302 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur\b\io;
 | |
| 
 | |
| use ArrayAccess;
 | |
| use Countable;
 | |
| use Exception;
 | |
| use Generator;
 | |
| use nulib\file\Stream;
 | |
| 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 nulib\php\time\DateTime;
 | |
| use nulib\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) {
 | |
|     $contents = Stream::nursery_compat_verifix($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] = $this->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;
 | |
|   }
 | |
| }
 |