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 $force=false): bool {
 | 
						|
    try {
 | 
						|
      if ($force || $this->shouldUpdate()) {
 | 
						|
        @unlink($this->ppFile);
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
    } finally {
 | 
						|
      $this->cleanup();
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
}
 |