intégration de nulib/cache
This commit is contained in:
parent
6cedfe9493
commit
651ba8c553
@ -48,6 +48,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bin": [
|
"bin": [
|
||||||
|
"php/bin/cachectl.php",
|
||||||
"php/bin/dumpser.php",
|
"php/bin/dumpser.php",
|
||||||
"php/bin/json2yml.php",
|
"php/bin/json2yml.php",
|
||||||
"php/bin/yml2json.php",
|
"php/bin/yml2json.php",
|
||||||
|
7
php/bin/cachectl.php
Executable file
7
php/bin/cachectl.php
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/php
|
||||||
|
<?php
|
||||||
|
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use cli\CachectlApp;
|
||||||
|
|
||||||
|
CachectlApp::run();
|
127
php/cli/CachectlApp.php
Normal file
127
php/cli/CachectlApp.php
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
namespace cli;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use nulib\app\cli\Application;
|
||||||
|
use nulib\cache\CacheFile;
|
||||||
|
use nulib\ext\yaml;
|
||||||
|
use nulib\os\path;
|
||||||
|
use nulib\output\msg;
|
||||||
|
|
||||||
|
class CachectlApp extends Application {
|
||||||
|
const ACTION_READ = 10, ACTION_INFOS = 20, ACTION_CLEAN = 30;
|
||||||
|
const ACTION_UPDATE = 40, ACTION_UPDATE_ADD = 41, ACTION_UPDATE_SUB = 42, ACTION_UPDATE_SET = 43;
|
||||||
|
|
||||||
|
const ARGS = [
|
||||||
|
"merge" => parent::ARGS,
|
||||||
|
"purpose" => "gestion de fichiers cache",
|
||||||
|
["-r", "--read", "name" => "action", "value" => self::ACTION_READ,
|
||||||
|
"help" => "Afficher le contenu d'un fichier cache",
|
||||||
|
],
|
||||||
|
["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS,
|
||||||
|
"help" => "Afficher des informations sur le fichier cache",
|
||||||
|
],
|
||||||
|
["-k", "--clean", "name" => "action", "value" => self::ACTION_CLEAN,
|
||||||
|
"help" => "Supprimer le fichier cache s'il a expiré",
|
||||||
|
],
|
||||||
|
["-a", "--add-duration", "args" => 1,
|
||||||
|
"action" => [null, "->setActionUpdate", self::ACTION_UPDATE_ADD],
|
||||||
|
"help" => "Ajouter le nombre de secondes spécifié à la durée du cache",
|
||||||
|
],
|
||||||
|
["-b", "--sub-duration", "args" => 1,
|
||||||
|
"action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SUB],
|
||||||
|
"help" => "Enlever le nombre de secondes spécifié à la durée du cache",
|
||||||
|
],
|
||||||
|
#XXX pas encore implémenté
|
||||||
|
//["-s", "--set-duration", "args" => 1,
|
||||||
|
// "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SET],
|
||||||
|
// "help" => "Mettre à jour la durée du cache à la valeur spécifiée",
|
||||||
|
//],
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $action = self::ACTION_READ;
|
||||||
|
|
||||||
|
protected $updateAction, $updateDuration;
|
||||||
|
|
||||||
|
function setActionUpdate(int $action, $updateDuration): void {
|
||||||
|
$this->action = self::ACTION_UPDATE;
|
||||||
|
switch ($action) {
|
||||||
|
case self::ACTION_UPDATE_SUB:
|
||||||
|
$this->updateAction = CacheFile::UPDATE_SUB;
|
||||||
|
break;
|
||||||
|
case self::ACTION_UPDATE_SET:
|
||||||
|
$this->updateAction = CacheFile::UPDATE_SET;
|
||||||
|
break;
|
||||||
|
case self::ACTION_UPDATE_ADD:
|
||||||
|
$this->updateAction = CacheFile::UPDATE_ADD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$this->updateDuration = $updateDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function findCaches(string $dir, ?array &$files): void {
|
||||||
|
foreach (glob("$dir/*") as $file) {
|
||||||
|
if (is_dir($file)) {
|
||||||
|
$this->findCaches($file, $files);
|
||||||
|
} elseif (is_file($file) && fnmatch("*.cache", $file)) {
|
||||||
|
$files[] = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
$files = [];
|
||||||
|
foreach ($this->args as $arg) {
|
||||||
|
if (is_dir($arg)) {
|
||||||
|
$this->findCaches($arg, $files);
|
||||||
|
} elseif (is_file($arg)) {
|
||||||
|
$files[] = $arg;
|
||||||
|
} else {
|
||||||
|
msg::warning("$arg: fichier introuvable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$showSection = count($files) > 1;
|
||||||
|
foreach ($files as $file) {
|
||||||
|
switch ($this->action) {
|
||||||
|
case self::ACTION_READ:
|
||||||
|
if ($showSection) msg::section($file);
|
||||||
|
$cache = new CacheFile($file, null, [
|
||||||
|
"readonly" => true,
|
||||||
|
"duration" => "INF",
|
||||||
|
"override_duration" => true,
|
||||||
|
]);
|
||||||
|
yaml::dump($cache->get());
|
||||||
|
break;
|
||||||
|
case self::ACTION_INFOS:
|
||||||
|
if ($showSection) msg::section($file);
|
||||||
|
$cache = new CacheFile($file, null, [
|
||||||
|
"readonly" => true,
|
||||||
|
]);
|
||||||
|
yaml::dump($cache->getInfos());
|
||||||
|
break;
|
||||||
|
case self::ACTION_CLEAN:
|
||||||
|
msg::action(path::ppath($file));
|
||||||
|
$cache = new CacheFile($file);
|
||||||
|
try {
|
||||||
|
if ($cache->deleteExpired()) msg::asuccess("fichier supprimé");
|
||||||
|
else msg::adone("fichier non expiré");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
msg::afailure($e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case self::ACTION_UPDATE:
|
||||||
|
msg::action(path::ppath($file));
|
||||||
|
$cache = new CacheFile($file);
|
||||||
|
try {
|
||||||
|
$cache->updateDuration($this->updateDuration, $this->updateAction);
|
||||||
|
msg::asuccess("fichier mis à jour");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
msg::afailure($e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
self::die("$this->action: action non implémentée");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
php/src/cache/CacheData.php
vendored
Normal file
46
php/src/cache/CacheData.php
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use nulib\cl;
|
||||||
|
use nulib\php\func;
|
||||||
|
use Traversable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface CacheData: gestion d'une donnée mise en cache
|
||||||
|
*/
|
||||||
|
abstract class CacheData {
|
||||||
|
function __construct(?string $name, $compute) {
|
||||||
|
$this->name = $name ?? "";
|
||||||
|
$this->compute = func::withn($compute ?? static::COMPUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string $name;
|
||||||
|
|
||||||
|
function getName() : string {
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ?func $compute;
|
||||||
|
|
||||||
|
/** calculer la donnée */
|
||||||
|
function compute() {
|
||||||
|
$compute = $this->compute;
|
||||||
|
$data = $compute !== null? $compute->invoke(): null;
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** spécifier le chemin du cache à partir du fichier de base */
|
||||||
|
abstract function setDatafile(?string $basefile): void;
|
||||||
|
|
||||||
|
/** indiquer si le cache existe */
|
||||||
|
abstract function exists(): bool;
|
||||||
|
|
||||||
|
/** charger la donnée depuis le cache */
|
||||||
|
abstract function load();
|
||||||
|
|
||||||
|
/** sauvegarder la donnée dans le cache et la retourner */
|
||||||
|
abstract function save($data);
|
||||||
|
|
||||||
|
/** supprimer le cache */
|
||||||
|
abstract function delete();
|
||||||
|
}
|
360
php/src/cache/CacheFile.php
vendored
Normal file
360
php/src/cache/CacheFile.php
vendored
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
68
php/src/cache/CacheManager.php
vendored
Normal file
68
php/src/cache/CacheManager.php
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use nulib\cl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CacheManager: un gestionnaire de cache permettant de désactiver la mise
|
||||||
|
* en cache d'une valeur dans le cadre d'une session.
|
||||||
|
*
|
||||||
|
* en effet, si on désactive le cache, il doit être réactivé après que la valeur
|
||||||
|
* est calculée, pour éviter qu'une valeur soit calculée encore et encore dans
|
||||||
|
* une session de travail
|
||||||
|
*/
|
||||||
|
class CacheManager {
|
||||||
|
function __construct(?array $includes=null, ?array $excludes=null) {
|
||||||
|
$this->shouldCaches = [];
|
||||||
|
$this->defaultCache = true;
|
||||||
|
$this->includes = $includes;
|
||||||
|
$this->excludes = $excludes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array tableau {id => shouldCache} indiquant si l'élément id doit être
|
||||||
|
* mis en cache
|
||||||
|
*/
|
||||||
|
protected array $shouldCaches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool valeur par défaut de shouldCache si la valeur n'est pas trouvée
|
||||||
|
* dans $shouldCache
|
||||||
|
*/
|
||||||
|
protected bool $defaultCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array|null groupes à toujours inclure dans le cache. pour les
|
||||||
|
* identifiants de ces groupe, {@link self::shouldCache()} retourne toujours
|
||||||
|
* true.
|
||||||
|
*
|
||||||
|
* $excludes est prioritaire par rapport à $includes
|
||||||
|
*/
|
||||||
|
protected ?array $includes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array|null groupes à exclure de la mise en cache. la mise en cache est
|
||||||
|
* toujours calculée pour les identifiants de ces groupes.
|
||||||
|
*/
|
||||||
|
protected ?array $excludes;
|
||||||
|
|
||||||
|
function setNoCache(bool $noCache=true, bool $reset=true): self {
|
||||||
|
if ($reset) $this->shouldCaches = [];
|
||||||
|
$this->defaultCache = !$noCache;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCache(string $id, ?string $groupId=null, bool $reset=true): bool {
|
||||||
|
if ($groupId !== null) {
|
||||||
|
$includes = $this->includes;
|
||||||
|
$shouldInclude = $includes !== null && in_array($groupId, $includes);
|
||||||
|
$excludes = $this->excludes;
|
||||||
|
$shouldExclude = $excludes !== null && in_array($groupId, $excludes);
|
||||||
|
if ($shouldInclude && !$shouldExclude) return true;
|
||||||
|
}
|
||||||
|
$cacheId = "$groupId-$id";
|
||||||
|
$shouldCache = cl::get($this->shouldCaches, $cacheId, $this->defaultCache);
|
||||||
|
$this->shouldCaches[$cacheId] = $reset?: $shouldCache;
|
||||||
|
return $shouldCache;
|
||||||
|
}
|
||||||
|
}
|
36
php/src/cache/CursorCacheData.php
vendored
Normal file
36
php/src/cache/CursorCacheData.php
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
class CursorCacheData extends CacheData {
|
||||||
|
function __construct(array $cursorId, $compute=null, ?CursorChannel $channel=null) {
|
||||||
|
$name = $cursorId["group_id"];
|
||||||
|
if ($name) $name .= "_";
|
||||||
|
$name .= $cursorId["id"];
|
||||||
|
parent::__construct($name, $compute);
|
||||||
|
$channel ??= (new CursorChannel($cursorId))->initStorage(cache::storage());
|
||||||
|
$this->channel = $channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDatafile(?string $basefile): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CursorChannel $channel;
|
||||||
|
|
||||||
|
function exists(): bool {
|
||||||
|
return $this->channel->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
return $this->channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save($data) {
|
||||||
|
if (!is_iterable($data)) $data = [$data];
|
||||||
|
$this->channel->rechargeAll($data);
|
||||||
|
return $this->channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delete() {
|
||||||
|
$this->channel->delete(null);
|
||||||
|
}
|
||||||
|
}
|
127
php/src/cache/CursorChannel.php
vendored
Normal file
127
php/src/cache/CursorChannel.php
vendored
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use IteratorAggregate;
|
||||||
|
use nulib\cl;
|
||||||
|
use nulib\db\CapacitorChannel;
|
||||||
|
use nulib\db\CapacitorStorage;
|
||||||
|
use nulib\php\func;
|
||||||
|
use Traversable;
|
||||||
|
|
||||||
|
class CursorChannel extends CapacitorChannel implements IteratorAggregate {
|
||||||
|
static function with($cursorId=null, ?iterable $rows=null, ?CapacitorStorage $storage=null): self {
|
||||||
|
$storage ??= cache::storage();
|
||||||
|
$channel = (new static($cursorId))->initStorage($storage);
|
||||||
|
if ($rows !== null) $channel->rechargeAll($rows);
|
||||||
|
return $channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAME = "cursor";
|
||||||
|
const TABLE_NAME = "cursor";
|
||||||
|
|
||||||
|
const COLUMN_DEFINITIONS = [
|
||||||
|
"group_id_" => "varchar(32) not null", // groupe de curseur
|
||||||
|
"id_" => "varchar(128) not null", // nom du curseur
|
||||||
|
"key_index_" => "integer not null",
|
||||||
|
"key_" => "varchar(128) not null",
|
||||||
|
"search_" => "varchar(255)",
|
||||||
|
|
||||||
|
"primary key (group_id_, id_, key_index_)",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ADD_COLUMNS = null;
|
||||||
|
|
||||||
|
protected function COLUMN_DEFINITIONS(): ?array {
|
||||||
|
return cl::merge(self::COLUMN_DEFINITIONS, static::ADD_COLUMNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|string $cursorId
|
||||||
|
*/
|
||||||
|
function __construct($cursorId) {
|
||||||
|
parent::__construct();
|
||||||
|
cache::verifix_id($cursorId);
|
||||||
|
[
|
||||||
|
"group_id" => $this->groupId,
|
||||||
|
"id" => $this->id,
|
||||||
|
] = $cursorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string $groupId;
|
||||||
|
|
||||||
|
protected string $id;
|
||||||
|
|
||||||
|
function getCursorId(): array {
|
||||||
|
return [
|
||||||
|
"group_id" => $this->groupId,
|
||||||
|
"id" => $this->id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseFilter(): ?array {
|
||||||
|
return [
|
||||||
|
"group_id_" => $this->groupId,
|
||||||
|
"id_" => $this->id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int $index = 0;
|
||||||
|
|
||||||
|
protected function getSearch($item): ?string {
|
||||||
|
$search = cl::filter_n(cl::with($item));
|
||||||
|
$search = implode(" ", $search);
|
||||||
|
return substr($search, 0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemValues($item, $key=null): ?array {
|
||||||
|
$index = $this->index++;
|
||||||
|
$key = $key ?? $index;
|
||||||
|
$key = substr(strval($key), 0, 128);
|
||||||
|
$addColumns = static::ADD_COLUMNS ?? [];
|
||||||
|
$addColumns = cl::select($item,
|
||||||
|
array_filter(array_keys($addColumns), function ($key) {
|
||||||
|
return is_string($key);
|
||||||
|
}));
|
||||||
|
return cl::merge($addColumns, [
|
||||||
|
"group_id_" => $this->groupId,
|
||||||
|
"id_" => $this->id,
|
||||||
|
"key_index_" => $index,
|
||||||
|
"key_" => $key,
|
||||||
|
"search_" => $this->getSearch($item),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset(bool $recreate=false): void {
|
||||||
|
$this->index = 0;
|
||||||
|
parent::reset($recreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
|
||||||
|
if ($items === null) return 0;
|
||||||
|
$count = 0;
|
||||||
|
if ($func !== null) $func = func::with($func, $args)->bind($this);
|
||||||
|
foreach ($items as $key => $item) {
|
||||||
|
$count += $this->charge($item, $func, [$key]);
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rechargeAll(?iterable $items): self {
|
||||||
|
$this->delete(null);
|
||||||
|
$this->index = 0;
|
||||||
|
$this->chargeAll($items);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIterator(): Traversable {
|
||||||
|
$rows = $this->dbAll([
|
||||||
|
"cols" => ["key_", "item__"],
|
||||||
|
"where" => $this->getBaseFilter(),
|
||||||
|
]);
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$key = $row["key_"];
|
||||||
|
$item = $this->unserialize($row["item__"]);
|
||||||
|
yield $key => $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
php/src/cache/DataCacheData.php
vendored
Normal file
59
php/src/cache/DataCacheData.php
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use nulib\cl;
|
||||||
|
use nulib\file;
|
||||||
|
use nulib\os\path;
|
||||||
|
use nulib\php\func;
|
||||||
|
use Traversable;
|
||||||
|
|
||||||
|
class DataCacheData extends CacheData {
|
||||||
|
/** @var string identifiant de cette donnée */
|
||||||
|
const NAME = null;
|
||||||
|
|
||||||
|
/** @var callable une fonction permettant de calculer la donnée */
|
||||||
|
const COMPUTE = null;
|
||||||
|
|
||||||
|
function __construct(?string $name=null, $compute=null, ?string $basefile=null) {
|
||||||
|
$name ??= static::NAME ?? "";
|
||||||
|
$compute ??= static::COMPUTE;
|
||||||
|
parent::__construct($name, $compute);
|
||||||
|
$this->setDatafile($basefile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compute() {
|
||||||
|
$data = parent::compute();
|
||||||
|
if ($data instanceof Traversable) $data = cl::all($data);
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string $datafile;
|
||||||
|
|
||||||
|
function setDatafile(?string $basefile): void {
|
||||||
|
if ($basefile === null) {
|
||||||
|
$basedir = ".";
|
||||||
|
$basename = "";
|
||||||
|
} else {
|
||||||
|
$basedir = path::dirname($basefile);
|
||||||
|
$basename = path::filename($basefile);
|
||||||
|
}
|
||||||
|
$this->datafile = "$basedir/.$basename.{$this->name}".cache::EXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exists(): bool {
|
||||||
|
return file_exists($this->datafile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
return file::reader($this->datafile)->unserialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save($data) {
|
||||||
|
file::writer($this->datafile)->serialize($data);
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delete(): void {
|
||||||
|
@unlink($this->datafile);
|
||||||
|
}
|
||||||
|
}
|
6
php/src/cache/TODO.md
vendored
Normal file
6
php/src/cache/TODO.md
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# nulib\cache
|
||||||
|
|
||||||
|
* [ ] CacheChannel: stocker aussi la clé primaire, ce qui permet de récupérer
|
||||||
|
la donnée correspondante dans la source?
|
||||||
|
|
||||||
|
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary
|
93
php/src/cache/cache.php
vendored
Normal file
93
php/src/cache/cache.php
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use nulib\app\app;
|
||||||
|
use nulib\db\CapacitorStorage;
|
||||||
|
use nulib\db\sqlite\SqliteStorage;
|
||||||
|
use nulib\ext\utils;
|
||||||
|
|
||||||
|
class cache {
|
||||||
|
/** @var string extension des fichiers de cache */
|
||||||
|
const EXT = ".cache";
|
||||||
|
|
||||||
|
protected static ?string $dbfile = null;
|
||||||
|
|
||||||
|
protected static function dbfile(): ?string {
|
||||||
|
return self::$dbfile ??= app::get()->getVarfile("cache.db");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static ?CapacitorStorage $storage = null;
|
||||||
|
|
||||||
|
static function storage(): CapacitorStorage {
|
||||||
|
return self::$storage ??= new SqliteStorage(self::dbfile());
|
||||||
|
}
|
||||||
|
|
||||||
|
static function set_storage(CapacitorStorage $storage): CapacitorStorage {
|
||||||
|
return self::$storage = $storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static ?CacheManager $manager = null;
|
||||||
|
|
||||||
|
static function manager(): CacheManager {
|
||||||
|
return self::$manager ??= new CacheManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function set_manager(CacheManager $manager): CacheManager {
|
||||||
|
return self::$manager = $manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function nc(bool $noCache=true, bool $reset=false): void {
|
||||||
|
self::manager()->setNoCache($noCache, $reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function should_cache(string $id, ?string $groupId=null, bool $reset=true): bool {
|
||||||
|
return self::manager()->shouldCache($id, $groupId, $reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function verifix_id(&$cacheId): void {
|
||||||
|
$cacheId ??= utils::uuidgen();
|
||||||
|
if (is_array($cacheId)) {
|
||||||
|
$keys = array_keys($cacheId);
|
||||||
|
if (array_key_exists("id", $cacheId)) $idKey = "id";
|
||||||
|
else $idKey = $keys[0] ?? null;
|
||||||
|
$id = strval($cacheId[$idKey] ?? "");
|
||||||
|
if (array_key_exists("group_id", $cacheId)) $groupIdKey = "group_id";
|
||||||
|
else $groupIdKey = $keys[1] ?? null;
|
||||||
|
$groupId = strval($cacheId[$groupIdKey] ?? "");
|
||||||
|
} else {
|
||||||
|
$id = strval($cacheId);
|
||||||
|
$groupId = "";
|
||||||
|
}
|
||||||
|
# si le groupe ou le nom sont trop grand, en faire un hash
|
||||||
|
if (strlen($groupId) > 32) $groupId = md5($groupId);
|
||||||
|
if (strlen($id) > 128) $id = substr($id, 0, 128 - 32).md5($id);
|
||||||
|
$cacheId = ["group_id" => $groupId, "id" => $id];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function new(array $cacheId, ?string $suffix, $data, ?array $params=null): CacheFile {
|
||||||
|
$file = $cacheId["group_id"];
|
||||||
|
if ($file) $file .= "_";
|
||||||
|
$file .= $cacheId["id"];
|
||||||
|
$file .= $suffix;
|
||||||
|
return new CacheFile($file, $data, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function cache($dataId, $data, ?array $params=null): CacheFile {
|
||||||
|
self::verifix_id($dataId);
|
||||||
|
return self::new($dataId, null, $data, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function get($dataId, $data, ?array $params=null) {
|
||||||
|
self::verifix_id($dataId);
|
||||||
|
$noCache = !self::should_cache($dataId["id"], $dataId["group_id"]);
|
||||||
|
$cache = self::new($dataId, null, $data, $params);
|
||||||
|
return $cache->get(null, $noCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function all($cursorId, $rows, ?array $params=null): ?iterable {
|
||||||
|
self::verifix_id($cursorId);
|
||||||
|
$noCache = !self::should_cache($cursorId["id"], $cursorId["group_id"]);
|
||||||
|
$cache = self::new($cursorId, "_rows", new CursorCacheData($cursorId, $rows), $params);
|
||||||
|
return $cache->get(null, $noCache);
|
||||||
|
}
|
||||||
|
}
|
1
php/tbin/.gitignore
vendored
1
php/tbin/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/*.db
|
/*.db
|
||||||
|
/*.cache
|
||||||
|
73
php/tbin/test-cache.php
Executable file
73
php/tbin/test-cache.php
Executable file
@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/php
|
||||||
|
<?php
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use nulib\cache\DataCacheData;
|
||||||
|
use nulib\cache\CacheFile;
|
||||||
|
use nulib\ext\yaml;
|
||||||
|
use nulib\os\sh;
|
||||||
|
|
||||||
|
function show(string $prefix, CacheFile $cache, bool $dumpInfos=true): void {
|
||||||
|
Txx("$prefix=", $cache->get());
|
||||||
|
if ($dumpInfos) {
|
||||||
|
yaml::dump($cache->getInfos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//system("rm -f *.cache .*.cache");
|
||||||
|
|
||||||
|
$what = [
|
||||||
|
"null",
|
||||||
|
"one",
|
||||||
|
"two",
|
||||||
|
"three",
|
||||||
|
];
|
||||||
|
$duration = 10;
|
||||||
|
|
||||||
|
if (in_array("null", $what)) {
|
||||||
|
$null = new CacheFile("null", null, [
|
||||||
|
"duration" => $duration,
|
||||||
|
]);
|
||||||
|
show("null", $null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array("one", $what)) {
|
||||||
|
$one = new class("one", null, [
|
||||||
|
"duration" => $duration,
|
||||||
|
]) extends CacheFile {
|
||||||
|
protected function compute() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
show("one", $one);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array("two", $what)) {
|
||||||
|
$two = new CacheFile("two", new DataCacheData(null, function () {
|
||||||
|
return 2;
|
||||||
|
}), [
|
||||||
|
"duration" => $duration,
|
||||||
|
]);
|
||||||
|
show("two", $two);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array("three", $what)) {
|
||||||
|
$data31 = new DataCacheData("data31name", function () {
|
||||||
|
return 31;
|
||||||
|
});
|
||||||
|
|
||||||
|
$data32 = new DataCacheData(null, function () {
|
||||||
|
return 32;
|
||||||
|
});
|
||||||
|
|
||||||
|
$three = new CacheFile("three", [
|
||||||
|
"data31" => $data31,
|
||||||
|
$data31, # name=data31name
|
||||||
|
"data32" => $data32,
|
||||||
|
$data32, # name=""
|
||||||
|
]);
|
||||||
|
Txx("three.0=", $three->get("data31"));
|
||||||
|
Txx("three.1=", $three->get("data31name"));
|
||||||
|
Txx("three.2=", $three->get("data32"));
|
||||||
|
Txx("three.3=", $three->get(""));
|
||||||
|
}
|
38
php/tests/cache/CursorChannelTest.php
vendored
Normal file
38
php/tests/cache/CursorChannelTest.php
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use nulib\output\msg;
|
||||||
|
|
||||||
|
class CursorChannelTest extends _TestCase {
|
||||||
|
const DATA = [
|
||||||
|
"fr" => ["a" => "un", "b" => "deux"],
|
||||||
|
"eng" => ["a" => "one", "b" => "two"],
|
||||||
|
["a" => 1, "b" => 2],
|
||||||
|
];
|
||||||
|
|
||||||
|
function testUsage() {
|
||||||
|
$channel = CursorChannel::with("numbers", self::DATA, self::$storage);
|
||||||
|
$count = 0;
|
||||||
|
foreach ($channel as $key => $item) {
|
||||||
|
msg::info("one: $key => {$item["a"]}");
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
self::assertSame(3, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testAddColumns() {
|
||||||
|
$channel = (new class("numbers") extends CursorChannel {
|
||||||
|
const NAME = "numbersac";
|
||||||
|
const TABLE_NAME = self::NAME;
|
||||||
|
const ADD_COLUMNS = [
|
||||||
|
"a" => "varchar(30)",
|
||||||
|
];
|
||||||
|
})->initStorage(self::$storage)->rechargeAll(self::DATA);
|
||||||
|
$count = 0;
|
||||||
|
foreach ($channel as $key => $item) {
|
||||||
|
msg::info("one: $key => {$item["a"]}");
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
self::assertSame(3, $count);
|
||||||
|
}
|
||||||
|
}
|
22
php/tests/cache/SourceDb.php
vendored
Normal file
22
php/tests/cache/SourceDb.php
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use nulib\db\sqlite\Sqlite;
|
||||||
|
|
||||||
|
class SourceDb extends Sqlite {
|
||||||
|
const MIGRATION = [
|
||||||
|
"create table source (pk integer primary key autoincrement, s varchar(255), i integer, b boolean)",
|
||||||
|
[self::class, "fill_data"],
|
||||||
|
];
|
||||||
|
|
||||||
|
static function fill_data(Sqlite $db): void {
|
||||||
|
$db->exec("insert into source (s, i, b) values (null, null, null)");
|
||||||
|
$db->exec("insert into source (s, i, b) values ('false', 0, 0)");
|
||||||
|
$db->exec("insert into source (s, i, b) values ('first', 1, 1)");
|
||||||
|
$db->exec("insert into source (s, i, b) values ('second', 2, 1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(__DIR__."/source.db");
|
||||||
|
}
|
||||||
|
}
|
23
php/tests/cache/_TestCase.php
vendored
Normal file
23
php/tests/cache/_TestCase.php
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use nulib\db\sqlite\SqliteStorage;
|
||||||
|
use nulib\output\msg;
|
||||||
|
use nulib\output\std\StdMessenger;
|
||||||
|
use nulib\tests\TestCase;
|
||||||
|
|
||||||
|
class _TestCase extends TestCase {
|
||||||
|
protected static SqliteStorage $storage;
|
||||||
|
|
||||||
|
static function setUpBeforeClass(): void {
|
||||||
|
parent::setUpBeforeClass();
|
||||||
|
msg::set_messenger_class(StdMessenger::class);
|
||||||
|
self::$storage = new SqliteStorage(__DIR__."/cache.db");
|
||||||
|
cache::set_storage(self::$storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function tearDownAfterClass(): void {
|
||||||
|
parent::tearDownAfterClass();
|
||||||
|
self::$storage->close();
|
||||||
|
}
|
||||||
|
}
|
108
php/tests/cache/cacheTest.php
vendored
Normal file
108
php/tests/cache/cacheTest.php
vendored
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\cache;
|
||||||
|
|
||||||
|
use nulib\cl;
|
||||||
|
use nulib\output\msg;
|
||||||
|
|
||||||
|
class cacheTest extends _TestCase {
|
||||||
|
const DATA = [
|
||||||
|
"fr" => ["a" => "un", "b" => "deux"],
|
||||||
|
"eng" => ["a" => "one", "b" => "two"],
|
||||||
|
["a" => 1, "b" => 2],
|
||||||
|
];
|
||||||
|
|
||||||
|
function gendata() {
|
||||||
|
msg::note("gendata");
|
||||||
|
foreach (self::DATA as $key => $item) {
|
||||||
|
msg::info("yield $key");
|
||||||
|
yield $key => $item;
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
msg::note("fin gendata");
|
||||||
|
}
|
||||||
|
|
||||||
|
function _testRows(iterable $rows, int $expectedCount) {
|
||||||
|
$count = 0;
|
||||||
|
foreach ($rows as $key => $row) {
|
||||||
|
$parts = ["got $key => {"];
|
||||||
|
$i = 0;
|
||||||
|
foreach ($row as $k => $v) {
|
||||||
|
if ($i++ > 0) $parts[] = ", ";
|
||||||
|
$parts[] = "$k=$v";
|
||||||
|
}
|
||||||
|
$parts[] = "}";
|
||||||
|
msg::info(implode("", $parts));
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
self::assertSame($expectedCount, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _testGet(string $dataId, int $expectedCount, callable $gencompute) {
|
||||||
|
msg::section($dataId);
|
||||||
|
cache::nc(true, true);
|
||||||
|
|
||||||
|
msg::step("premier");
|
||||||
|
$rows = cache::get($dataId, $gencompute());
|
||||||
|
$this->_testRows($rows, $expectedCount);
|
||||||
|
msg::step("deuxième");
|
||||||
|
$rows = cache::get($dataId, $gencompute());
|
||||||
|
$this->_testRows($rows, $expectedCount);
|
||||||
|
|
||||||
|
msg::step("vider le cache");
|
||||||
|
cache::nc(true, true);
|
||||||
|
|
||||||
|
msg::step("premier");
|
||||||
|
$rows = cache::get($dataId, $gencompute());
|
||||||
|
$this->_testRows($rows, $expectedCount);
|
||||||
|
msg::step("deuxième");
|
||||||
|
$rows = cache::get($dataId, $gencompute());
|
||||||
|
$this->_testRows($rows, $expectedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetStatic() {
|
||||||
|
$this->_testGet("getStatic", 3, function () {
|
||||||
|
return static function () {
|
||||||
|
msg::note("getdata");
|
||||||
|
return self::DATA;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetGenerator() {
|
||||||
|
$this->_testGet("getGenerator", 3, function () {
|
||||||
|
return $this->gendata();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _testAll(string $cursorId, int $expectedCount, callable $gencompute) {
|
||||||
|
msg::section($cursorId);
|
||||||
|
cache::nc(true, true);
|
||||||
|
|
||||||
|
msg::step("premier");
|
||||||
|
$rows = cache::all($cursorId, $gencompute());
|
||||||
|
$this->_testRows($rows, $expectedCount);
|
||||||
|
msg::step("deuxième");
|
||||||
|
$rows = cache::all($cursorId, $gencompute());
|
||||||
|
$this->_testRows($rows, $expectedCount);
|
||||||
|
|
||||||
|
msg::step("vider le cache");
|
||||||
|
cache::nc(true, true);
|
||||||
|
|
||||||
|
msg::step("premier");
|
||||||
|
$rows = cache::all($cursorId, $gencompute());
|
||||||
|
$this->_testRows($rows, $expectedCount);
|
||||||
|
msg::step("deuxième");
|
||||||
|
$rows = cache::all($cursorId, $gencompute());
|
||||||
|
$this->_testRows($rows, $expectedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testAllGenerator() {
|
||||||
|
$this->_testAll("allGenerator", 4, function() {
|
||||||
|
return static function() {
|
||||||
|
$db = new SourceDb();
|
||||||
|
msg::note("query source");
|
||||||
|
yield from $db->all("select * from source");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user