<pman>Intégration de la branche dev74
This commit is contained in:
commit
019f4333c0
47
TODO.md
47
TODO.md
@ -73,6 +73,53 @@
|
||||
~~~
|
||||
bien entendu, bien que ce ne soit pas démontré ici, le premier argument
|
||||
(l'en-tête) est un contenu, pas seulement une chaine
|
||||
|
||||
chaque élément de contenu défini header, body et footer
|
||||
~~~php
|
||||
$element = ["header" => [], "body" => [], "footer" => []];
|
||||
~~~
|
||||
|
||||
colspan peut être utilisé dans header, body ou footer. si spécifié dans
|
||||
$element, colspan s'applique à body
|
||||
~~~php
|
||||
# les deux suivants sont équivalents
|
||||
$element = ["myheader", "mybody", "colspan" => 2, "myfooter"];
|
||||
$element = [
|
||||
"header" => [
|
||||
"content" => "myheader",
|
||||
],
|
||||
"body" => [
|
||||
"content" => "mybody",
|
||||
"colspan" => 2,
|
||||
],
|
||||
"footer" => [
|
||||
"content" => "myfooter",
|
||||
],
|
||||
];
|
||||
~~~
|
||||
|
||||
si $element est une fonction, il retourne la valeur de l'élement
|
||||
~~~php
|
||||
$element = function() {
|
||||
return ["header", "body"];
|
||||
}
|
||||
~~~
|
||||
|
||||
clé "enabled" permet d'activer dynamiquement un élément
|
||||
~~~php
|
||||
$element = function() {
|
||||
return ["header", "body", "enabled" => $includeElement];
|
||||
}
|
||||
~~~
|
||||
|
||||
clé "func" pour spécifier la fonction dans un contexte statique
|
||||
~~~php
|
||||
$element = ["func" => [My::class, "get_element"]];
|
||||
~~~
|
||||
func n'est considéré que si $element était un tableau à la base
|
||||
s'il y a d'autres clés, elles sont fusionnées avec le résultat de $func()
|
||||
(dont les clés sont prioritaires)
|
||||
|
||||
* Cursor::dyn permet d'insérer une valeur qui sera évaluée plus tard lors de la
|
||||
résolution du contenu
|
||||
* pour Cursor, CTable, etc., un paramètre "params_func" permet de générer une
|
||||
|
@ -99,6 +99,9 @@ class icon {
|
||||
static final function plus($suffix=null, ?string $alt=null): array { return self::manager()->getIcon("plus", $suffix, $alt); }
|
||||
static final function minus($suffix=null, ?string $alt=null): array { return self::manager()->getIcon("minus", $suffix, $alt); }
|
||||
|
||||
static final function start($suffix=null, ?string $alt=null): array { return self::manager()->getIcon("play", $suffix, $alt); }
|
||||
static final function stop($suffix=null, ?string $alt=null): array { return self::manager()->getIcon("stop", $suffix, $alt); }
|
||||
|
||||
# template:
|
||||
#static final function xxx($suffix=null, ?string $alt=null): array { return self::manager()->getIcon("xxx", $suffix, $alt); }
|
||||
}
|
||||
|
184
src/cache/CacheChannel.php
vendored
184
src/cache/CacheChannel.php
vendored
@ -1,116 +1,128 @@
|
||||
<?php
|
||||
namespace nulib\cache;
|
||||
|
||||
use IteratorAggregate;
|
||||
use nulib\cl;
|
||||
use nulib\db\CapacitorChannel;
|
||||
use nulib\php\time\DateTime;
|
||||
use nulib\php\time\Delay;
|
||||
use nulib\db\CapacitorStorage;
|
||||
use nulib\ext\utils;
|
||||
use nulib\php\func;
|
||||
use Traversable;
|
||||
|
||||
class CacheChannel extends CapacitorChannel {
|
||||
/** @var int durée de vie par défaut du cache */
|
||||
const DURATION = "1D"; // jusqu'au lendemain
|
||||
class CacheChannel extends CapacitorChannel implements IteratorAggregate {
|
||||
static function with(?iterable $rows=null, $cursorId=null, ?CapacitorStorage $storage=null): self {
|
||||
$storage ??= cache::storage();
|
||||
$channel = (new static($cursorId))->initStorage($storage);
|
||||
if ($rows !== null) $channel->build($rows);
|
||||
return $channel;
|
||||
}
|
||||
|
||||
const INCLUDES = null;
|
||||
|
||||
const EXCLUDES = null;
|
||||
const NAME = "cache";
|
||||
const TABLE_NAME = "cache";
|
||||
|
||||
const COLUMN_DEFINITIONS = [
|
||||
"group_id" => "varchar(64) not null",
|
||||
"id" => "varchar(64) not null",
|
||||
"date_start" => "datetime",
|
||||
"duration_" => "text",
|
||||
"primary key (group_id, id)",
|
||||
"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_)",
|
||||
];
|
||||
|
||||
static function get_cache_ids($id): array {
|
||||
if (is_array($id)) {
|
||||
$keys = array_keys($id);
|
||||
if (array_key_exists("group_id", $id)) $groupIdKey = "group_id";
|
||||
else $groupIdKey = $keys[1] ?? null;
|
||||
$groupId = $id[$groupIdKey] ?? "";
|
||||
if (array_key_exists("id", $id)) $idKey = "id";
|
||||
else $idKey = $keys[0] ?? null;
|
||||
$id = $id[$idKey] ?? "";
|
||||
} else {
|
||||
$groupId = "";
|
||||
}
|
||||
if (preg_match('/^(.*\\\\)?([^\\\\]+)$/', $groupId, $ms)) {
|
||||
# si le groupe est une classe, faire un hash du package pour limiter la
|
||||
# longueur du groupe
|
||||
[$package, $groupId] = [$ms[1], $ms[2]];
|
||||
$package = substr(md5($package), 0, 4);
|
||||
$groupId = "${groupId}_$package";
|
||||
}
|
||||
return ["group_id" => $groupId, "id" => $id];
|
||||
const ADD_COLUMNS = null;
|
||||
|
||||
protected function COLUMN_DEFINITIONS(): ?array {
|
||||
return cl::merge(self::COLUMN_DEFINITIONS, static::ADD_COLUMNS);
|
||||
}
|
||||
|
||||
function __construct(?string $duration=null, ?string $name=null) {
|
||||
parent::__construct($name);
|
||||
$this->duration = $duration ?? static::DURATION;
|
||||
$this->includes = static::INCLUDES;
|
||||
$this->excludes = static::EXCLUDES;
|
||||
/**
|
||||
* @param array|string $cursorId
|
||||
*/
|
||||
function __construct($cursorId) {
|
||||
parent::__construct();
|
||||
cache::verifix_id($cursorId);
|
||||
[
|
||||
"group_id" => $this->groupId,
|
||||
"id" => $this->id,
|
||||
] = $cursorId;
|
||||
}
|
||||
|
||||
protected string $duration;
|
||||
protected string $groupId;
|
||||
|
||||
protected ?array $includes;
|
||||
protected string $id;
|
||||
|
||||
protected ?array $excludes;
|
||||
function getCursorId(): array {
|
||||
return [
|
||||
"group_id" => $this->groupId,
|
||||
"id" => $this->id,
|
||||
];
|
||||
}
|
||||
|
||||
function getItemValues($item): ?array {
|
||||
return cl::merge(self::get_cache_ids($item), [
|
||||
"item" => null,
|
||||
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 onCreate($item, array $row, ?array $alwaysNull, ?string $duration=null): ?array {
|
||||
$now = new DateTime();
|
||||
$duration ??= $this->duration;
|
||||
return [
|
||||
"date_start" => $now,
|
||||
"duration" => new Delay($duration, $now),
|
||||
];
|
||||
function reset(bool $recreate=false): void {
|
||||
$this->index = 0;
|
||||
parent::reset($recreate);
|
||||
}
|
||||
|
||||
function onUpdate($item, array $row, array $prow, ?string $duration=null): ?array {
|
||||
$now = new DateTime();
|
||||
$duration ??= $this->duration;
|
||||
return [
|
||||
"date_start" => $now,
|
||||
"duration" => new Delay($duration, $now),
|
||||
];
|
||||
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
|
||||
$this->index = 0;
|
||||
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 shouldUpdate($id, bool $noCache=false): bool {
|
||||
if ($noCache) return true;
|
||||
|
||||
$cacheIds = self::get_cache_ids($id);
|
||||
$groupId = $cacheIds["group_id"];
|
||||
if ($groupId) {
|
||||
$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;
|
||||
function build(?iterable $items): self {
|
||||
$this->delete(null);
|
||||
$this->chargeAll($items);
|
||||
return $this;
|
||||
}
|
||||
|
||||
$found = false;
|
||||
$expired = false;
|
||||
$this->each($cacheIds,
|
||||
function($row) use (&$found, &$expired) {
|
||||
$found = true;
|
||||
$expired = $row["duration"]->isElapsed();
|
||||
});
|
||||
return !$found || $expired;
|
||||
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;
|
||||
}
|
||||
|
||||
function setCached($id, ?string $duration=null): void {
|
||||
$cacheIds = self::get_cache_ids($id);
|
||||
$this->charge($cacheIds, null, [$duration]);
|
||||
}
|
||||
|
||||
function resetCached($id) {
|
||||
$cacheIds = self::get_cache_ids($id);
|
||||
$this->delete($cacheIds);
|
||||
}
|
||||
}
|
||||
|
53
src/cache/CacheDataChannel.php
vendored
53
src/cache/CacheDataChannel.php
vendored
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
namespace nulib\cache;
|
||||
|
||||
use Closure;
|
||||
use IteratorAggregate;
|
||||
use nulib\cache\CacheChannel;
|
||||
use nulib\cache\cache;
|
||||
use nulib\cl;
|
||||
use nulib\db\CapacitorChannel;
|
||||
use Traversable;
|
||||
|
||||
class CacheDataChannel extends CapacitorChannel implements IteratorAggregate {
|
||||
const COLUMN_DEFINITIONS = [
|
||||
"key" => "varchar(128) primary key not null",
|
||||
"all_values" => "mediumtext",
|
||||
];
|
||||
|
||||
function __construct($id, callable $builder, ?string $duration=null) {
|
||||
$this->cacheIds = $cacheIds = CacheChannel::get_cache_ids($id);
|
||||
$this->builder = Closure::fromCallable($builder);
|
||||
$this->duration = $duration;
|
||||
$name = "{$cacheIds["group_id"]}-{$cacheIds["id"]}";
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected array $cacheIds;
|
||||
|
||||
protected Closure $builder;
|
||||
|
||||
protected ?string $duration = null;
|
||||
|
||||
function getItemValues($item): ?array {
|
||||
$key = array_keys($item)[0];
|
||||
$row = $item[$key];
|
||||
return [
|
||||
"key" => $key,
|
||||
"item" => $row,
|
||||
"all_values" => implode(" ", cl::filter_n(cl::with($row))),
|
||||
];
|
||||
}
|
||||
|
||||
function getIterator(): Traversable {
|
||||
$cm = cache::get();
|
||||
if ($cm->shouldUpdate($this->cacheIds)) {
|
||||
$this->capacitor->reset();
|
||||
foreach (($this->builder)() as $key => $row) {
|
||||
$this->charge([$key => $row]);
|
||||
}
|
||||
$cm->setCached($this->cacheIds, $this->duration);
|
||||
}
|
||||
return $this->discharge(false);
|
||||
}
|
||||
}
|
106
src/cache/CacheFile.php
vendored
106
src/cache/CacheFile.php
vendored
@ -3,6 +3,7 @@ namespace nulib\cache;
|
||||
|
||||
use Exception;
|
||||
use nulib\cv;
|
||||
use nulib\ext\utils;
|
||||
use nulib\file;
|
||||
use nulib\file\SharedFile;
|
||||
use nulib\os\path;
|
||||
@ -18,6 +19,11 @@ class CacheFile extends SharedFile {
|
||||
|
||||
const EXT = ".cache";
|
||||
|
||||
static function with($data, ?string $file=null): self {
|
||||
if ($data instanceof self) return $data;
|
||||
else return new static($file, $data);
|
||||
}
|
||||
|
||||
protected function ensure_source($source): CacheData {
|
||||
if ($source instanceof CacheData) return $source;
|
||||
if (cv::subclass_of($source, CacheData::class)) return new $source();
|
||||
@ -25,11 +31,8 @@ class CacheFile extends SharedFile {
|
||||
throw ValueException::invalid_type($source, CacheData::class);
|
||||
}
|
||||
|
||||
function __construct($file, ?array $params=null) {
|
||||
if ($file === null) {
|
||||
$rand = bin2hex(random_bytes(8));
|
||||
$file = path::join(sys_get_temp_dir(), $rand);
|
||||
}
|
||||
function __construct(?string $file, $data=null, ?array $params=null) {
|
||||
$file ??= path::join(sys_get_temp_dir(), utils::uuidgen());
|
||||
$file = path::ensure_ext($file, self::EXT);
|
||||
$this->basedir = path::dirname($file);
|
||||
$basename = path::filename($file);
|
||||
@ -38,7 +41,7 @@ class CacheFile extends SharedFile {
|
||||
$this->overrideDuration = $params["override_duration"] ?? false;
|
||||
$this->readonly = $params["readonly"] ?? false;
|
||||
$this->cacheNull = $params["cache_null"] ?? false;
|
||||
$data = $params["data"] ?? null;
|
||||
$data ??= $params["data"] ?? null;
|
||||
$this->sources = null;
|
||||
if ($data === null) {
|
||||
$this->source = null;
|
||||
@ -112,6 +115,8 @@ class CacheFile extends SharedFile {
|
||||
|
||||
protected ?Delay $duration;
|
||||
|
||||
protected $data;
|
||||
|
||||
protected ?array $datafilenames;
|
||||
|
||||
/** charger les données. le fichier a déjà été verrouillé en lecture */
|
||||
@ -121,6 +126,7 @@ class CacheFile extends SharedFile {
|
||||
[
|
||||
"start" => $start,
|
||||
"duration" => $duration,
|
||||
"data" => $data,
|
||||
"datafiles" => $datafilenames,
|
||||
] = $this->unserialize(null, false, true);
|
||||
if ($this->overrideDuration) {
|
||||
@ -130,10 +136,12 @@ class CacheFile extends SharedFile {
|
||||
} else {
|
||||
$start = null;
|
||||
$duration = null;
|
||||
$data = null;
|
||||
$datafilenames = [];
|
||||
}
|
||||
$this->start = $start;
|
||||
$this->duration = $duration;
|
||||
$this->data = $data;
|
||||
$this->datafilenames = $datafilenames;
|
||||
}
|
||||
|
||||
@ -163,15 +171,16 @@ class CacheFile extends SharedFile {
|
||||
$this->serialize([
|
||||
"start" => $this->start,
|
||||
"duration" => $this->duration,
|
||||
"data" => $this->data,
|
||||
"datafiles" => $datafilenames,
|
||||
], false, true);
|
||||
}
|
||||
|
||||
protected function loadData(string $datafile) {
|
||||
protected function loadDatafile(string $datafile) {
|
||||
return file::reader($datafile)->unserialize();
|
||||
}
|
||||
|
||||
protected function saveData(string $datafile, $data): void {
|
||||
protected function saveDatafile(string $datafile, $data): void {
|
||||
file::writer($datafile)->serialize($data);
|
||||
}
|
||||
|
||||
@ -182,10 +191,10 @@ class CacheFile extends SharedFile {
|
||||
}
|
||||
|
||||
protected function unlinkFiles(bool $datafilesOnly=false): void {
|
||||
if (!$datafilesOnly) @unlink($this->file);
|
||||
foreach ($this->datafilenames as $datafilename) {
|
||||
$this->unlinkDatafile($datafilename);
|
||||
}
|
||||
if (!$datafilesOnly) @unlink($this->file);
|
||||
}
|
||||
|
||||
/** tester si $value peut être mis en cache */
|
||||
@ -197,26 +206,28 @@ class CacheFile extends SharedFile {
|
||||
|
||||
protected ?Delay $oduration;
|
||||
|
||||
protected $odata;
|
||||
|
||||
protected ?array $odatafilenames;
|
||||
|
||||
protected function beforeAction() {
|
||||
$this->loadMetadata();
|
||||
$this->ostart = cv::clone($this->start);
|
||||
$this->oduration = cv::clone($this->duration);
|
||||
$this->odata = cv::clone($this->data);
|
||||
$this->odatafilenames = cv::clone($this->datafilenames);
|
||||
}
|
||||
|
||||
protected function afterAction() {
|
||||
$start = $this->start;
|
||||
$modified = false;
|
||||
if ($this->start != $this->ostart) $modified = true;
|
||||
$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;
|
||||
# égalité stricte uniquement pour $data et $datafiles
|
||||
if ($this->data !== $this->odata) $modified = true;
|
||||
if ($this->datafilenames !== $this->odatafilenames) $modified = true;
|
||||
if ($modified && !$this->readonly) {
|
||||
$this->lockWrite();
|
||||
$this->saveMetadata();
|
||||
@ -232,8 +243,13 @@ class CacheFile extends SharedFile {
|
||||
$this->afterAction();
|
||||
return $result;
|
||||
} finally {
|
||||
$this->ostart = null;
|
||||
$this->oduration = null;
|
||||
$this->odata = null;
|
||||
$this->odatafilenames = null;
|
||||
$this->start = null;
|
||||
$this->duration = null;
|
||||
$this->data = null;
|
||||
$this->datafilenames = null;
|
||||
$this->unlock(true);
|
||||
}
|
||||
@ -243,15 +259,16 @@ class CacheFile extends SharedFile {
|
||||
return null;
|
||||
}
|
||||
|
||||
function get($data=null, bool $noCache=false) {
|
||||
return $this->action(function () use ($data, $noCache) {
|
||||
protected function refreshData($data, bool $noCache) {
|
||||
$defaultSource = $data === null;
|
||||
$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 ($defaultSource) $updateData = $this->data === null;
|
||||
else $updateData = !array_key_exists($datafilename, $this->datafilenames);
|
||||
if (!$this->readonly && ($updateMetadata || $updateData)) {
|
||||
$this->lockWrite();
|
||||
if ($updateMetadata) {
|
||||
@ -259,35 +276,78 @@ class CacheFile extends SharedFile {
|
||||
$this->unlinkFiles(true);
|
||||
$this->start = null;
|
||||
$this->duration = null;
|
||||
$this->data = null;
|
||||
$updateData = true;
|
||||
}
|
||||
if ($defaultSource) {
|
||||
if ($updateData) {
|
||||
# calculer un fichier
|
||||
# calculer la valeur
|
||||
try {
|
||||
$data = $source->get();
|
||||
} catch (Exception $e) {
|
||||
# ne pas garder le fichier en cas d'exception
|
||||
# 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->get();
|
||||
} catch (Exception $e) {
|
||||
# ne pas garder le fichier destination en cas d'exception
|
||||
$this->unlinkDatafile($datafile);
|
||||
throw $e;
|
||||
}
|
||||
} elseif (file_exists($datafile)) {
|
||||
$data = $this->loadData($datafile);
|
||||
$data = $this->loadDatafile($datafile);
|
||||
} else {
|
||||
$data = null;
|
||||
}
|
||||
if ($this->shouldCache($data)) {
|
||||
$this->saveData($datafile, $data);
|
||||
$this->saveDatafile($datafile, $data);
|
||||
$this->datafilenames[$datafilename] = true;
|
||||
} else {
|
||||
# ne pas garder le fichier s'il ne faut pas mettre en cache
|
||||
$this->unlinkDatafile($datafile);
|
||||
}
|
||||
}
|
||||
} elseif ($defaultSource) {
|
||||
$data = $this->data;
|
||||
} elseif (file_exists($datafile)) {
|
||||
$data = $this->loadData($datafile);
|
||||
$data = $this->loadDatafile($datafile);
|
||||
} 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) {
|
||||
$source = $this->source;
|
||||
if ($source !== null) $datas = [null];
|
||||
else $datas = array_keys($this->sources);
|
||||
foreach ($datas 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
68
src/cache/CacheManager.php
vendored
Normal file
68
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;
|
||||
}
|
||||
}
|
72
src/cache/cache.php
vendored
72
src/cache/cache.php
vendored
@ -2,11 +2,10 @@
|
||||
namespace nulib\cache;
|
||||
|
||||
use nulib\app;
|
||||
use nulib\cache\CacheChannel;
|
||||
use nulib\cache\CacheDataChannel;
|
||||
use nulib\db\Capacitor;
|
||||
use nulib\db\CapacitorStorage;
|
||||
use nulib\db\sqlite\SqliteStorage;
|
||||
use nulib\ext\utils;
|
||||
use nulib\php\func;
|
||||
|
||||
class cache {
|
||||
protected static ?string $dbfile = null;
|
||||
@ -17,30 +16,67 @@ class cache {
|
||||
|
||||
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 function get_storage(): CapacitorStorage {
|
||||
return self::$storage ??= new SqliteStorage(self::dbfile());
|
||||
protected static ?CacheManager $manager = null;
|
||||
|
||||
static function manager(): CacheManager {
|
||||
return self::$manager ??= new CacheManager();
|
||||
}
|
||||
|
||||
protected static ?CacheChannel $channel = null;
|
||||
|
||||
static function set(?CacheChannel $channel): CacheChannel {
|
||||
$channel ??= new CacheChannel();
|
||||
new Capacitor(self::get_storage(), $channel);
|
||||
return self::$channel = $channel;
|
||||
static function set_manager(CacheManager $manager): CacheManager {
|
||||
return self::$manager = $manager;
|
||||
}
|
||||
|
||||
static function get(): CacheChannel {
|
||||
if (self::$channel !== null) return self::$channel;
|
||||
else return self::set(null);
|
||||
static function nc(bool $noCache=true, bool $reset=false): void {
|
||||
self::manager()->setNoCache($noCache, $reset);
|
||||
}
|
||||
|
||||
static function new(?CacheDataChannel $channel, $id=null, ?callable $builder=null): CacheDataChannel {
|
||||
$channel ??= new CacheDataChannel($id, $builder);
|
||||
new Capacitor(self::get_storage(), $channel);
|
||||
return $channel;
|
||||
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];
|
||||
}
|
||||
|
||||
static function get(callable $compute, $dataId=null, ?string $file=null) {
|
||||
self::verifix_id($dataId);
|
||||
$file ??= "{$dataId["group_id"]}_{$dataId["id"]}";
|
||||
$noCache = !self::should_cache($dataId["id"], $dataId["group_id"]);
|
||||
return CacheFile::with($compute, $file)->get(null, $noCache);
|
||||
}
|
||||
|
||||
static function all(?iterable $rows, $cursorId=null, ?string $file=null): iterable {
|
||||
self::verifix_id($cursorId);
|
||||
$file ??= "{$cursorId["group_id"]}_{$cursorId["id"]}_rows";
|
||||
$ccursorId = new CacheFile($file, function() use ($rows, $cursorId) {
|
||||
CacheChannel::with(null, $cursorId)->build($rows);
|
||||
return $cursorId;
|
||||
});
|
||||
$noCache = !self::should_cache($cursorId["id"], $cursorId["group_id"]);
|
||||
return CacheChannel::with(null, $ccursorId->get(null, $noCache));
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ abstract class AbstractStorageApp extends Application {
|
||||
}
|
||||
}
|
||||
$first = true;
|
||||
$capacitor->each($filter, function ($item, $row) use (&$first) {
|
||||
$capacitor->each($filter, function ($row) use (&$first) {
|
||||
if ($first) $first = false;
|
||||
else echo "---\n";
|
||||
yaml::dump($row);
|
||||
|
@ -87,7 +87,7 @@ class NucacheApp extends Application {
|
||||
switch ($this->action) {
|
||||
case self::ACTION_READ:
|
||||
if ($showSection) msg::section($file);
|
||||
$cache = new CacheFile($file, [
|
||||
$cache = new CacheFile($file, null, [
|
||||
"readonly" => true,
|
||||
"duration" => "INF",
|
||||
"override_duration" => true,
|
||||
@ -96,7 +96,7 @@ class NucacheApp extends Application {
|
||||
break;
|
||||
case self::ACTION_INFOS:
|
||||
if ($showSection) msg::section($file);
|
||||
$cache = new CacheFile($file, [
|
||||
$cache = new CacheFile($file, null, [
|
||||
"readonly" => true,
|
||||
]);
|
||||
yaml::dump($cache->getInfos());
|
||||
|
@ -21,6 +21,33 @@
|
||||
|
||||
* `ScalarSchema::from_property()`
|
||||
|
||||
* pour le support des propriétés des objets, il faudrait pouvoir spécifier
|
||||
comment instancier l'objet. je ne sais pas si ça doit se mettre au niveau du
|
||||
type, du schéma, ou autre
|
||||
~~~php
|
||||
Schema::ns($schema, [
|
||||
"rt" => ["?string", "required" => true],
|
||||
"rtd" => ["?int", "required" => true, "default" => 42],
|
||||
"ot" => ["?int"],
|
||||
"otd" => ["?string", "default" => "hello"],
|
||||
"ot2" => ["int"],
|
||||
"" => ["assoc",
|
||||
"class" => MyClass::class,
|
||||
],
|
||||
]);
|
||||
|
||||
# peut provisionner la classe suivante
|
||||
class MyClass {
|
||||
public ?string $rt;
|
||||
public ?int $rtd = 42;
|
||||
public ?int $ot = null;
|
||||
public ?string $otd = "hello";
|
||||
public int $ot2 = 0;
|
||||
}
|
||||
~~~
|
||||
il y a potentiellement un problème d'oeuf et de poule si on se sert de ce
|
||||
genre de définitions pour autogénérer la classe
|
||||
|
||||
* l'argument $format de AssocWrapper::format() est un tableau associatif
|
||||
`[$key => $format]`
|
||||
cela permet de spécifier des format spécifiques pour certains champs.
|
||||
|
@ -17,22 +17,22 @@ function show(string $prefix, CacheFile $cache, bool $dumpInfos=true): void {
|
||||
//system("rm -f *.cache .*.cache");
|
||||
|
||||
$what = [
|
||||
//"null",
|
||||
"null",
|
||||
"one",
|
||||
//"two",
|
||||
//"three",
|
||||
"two",
|
||||
"three",
|
||||
];
|
||||
$duration = 10;
|
||||
|
||||
if (in_array("null", $what)) {
|
||||
$null = new CacheFile("null", [
|
||||
$null = new CacheFile("null", null, [
|
||||
"duration" => $duration,
|
||||
]);
|
||||
show("null", $null);
|
||||
}
|
||||
|
||||
if (in_array("one", $what)) {
|
||||
$one = new class("one", [
|
||||
$one = new class("one", null, [
|
||||
"duration" => $duration,
|
||||
]) extends CacheFile {
|
||||
protected function compute() {
|
||||
@ -43,11 +43,10 @@ if (in_array("one", $what)) {
|
||||
}
|
||||
|
||||
if (in_array("two", $what)) {
|
||||
$two = new CacheFile("two", [
|
||||
"duration" => $duration,
|
||||
"data" => new CacheData(function () {
|
||||
$two = new CacheFile("two", new CacheData(function () {
|
||||
return 2;
|
||||
}),
|
||||
}), [
|
||||
"duration" => $duration,
|
||||
]);
|
||||
show("two", $two);
|
||||
}
|
||||
@ -62,12 +61,10 @@ if (in_array("three", $what)) {
|
||||
});
|
||||
|
||||
$three = new CacheFile("three", [
|
||||
"data" => [
|
||||
"data31" => $data31,
|
||||
$data31, # name=data31name
|
||||
"data32" => $data32,
|
||||
$data32, # name=""
|
||||
],
|
||||
]);
|
||||
Txx("three.0=", $three->get("data31"));
|
||||
Txx("three.1=", $three->get("data31name"));
|
||||
|
2
tests/.gitignore
vendored
Normal file
2
tests/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.db*
|
||||
*.cache
|
38
tests/cache/CacheChannelTest.php
vendored
Normal file
38
tests/cache/CacheChannelTest.php
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace nulib\cache;
|
||||
|
||||
use nulib\output\msg;
|
||||
|
||||
class CacheChannelTest extends _TestCase {
|
||||
const DATA = [
|
||||
"fr" => ["a" => "un", "b" => "deux"],
|
||||
"eng" => ["a" => "one", "b" => "two"],
|
||||
["a" => 1, "b" => 2],
|
||||
];
|
||||
|
||||
function testUsage() {
|
||||
$channel = CacheChannel::with(self::DATA, "numbers", 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 CacheChannel {
|
||||
const NAME = "numbersac";
|
||||
const TABLE_NAME = self::NAME;
|
||||
const ADD_COLUMNS = [
|
||||
"a" => "varchar(30)",
|
||||
];
|
||||
})->initStorage(self::$storage)->build(self::DATA);
|
||||
$count = 0;
|
||||
foreach ($channel as $key => $item) {
|
||||
msg::info("one: $key => {$item["a"]}");
|
||||
$count++;
|
||||
}
|
||||
self::assertSame(3, $count);
|
||||
}
|
||||
}
|
23
tests/cache/_TestCase.php
vendored
Normal file
23
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();
|
||||
}
|
||||
}
|
55
tests/cache/cacheTest.php
vendored
Normal file
55
tests/cache/cacheTest.php
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
<?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() {
|
||||
foreach (self::DATA as $key => $item) {
|
||||
msg::info("yield $key");
|
||||
yield $key => $item;
|
||||
sleep(2);
|
||||
}
|
||||
msg::info("fin gendata");
|
||||
}
|
||||
|
||||
function _testRows(iterable $rows) {
|
||||
$count = 0;
|
||||
foreach ($rows as $key => $row) {
|
||||
msg::info("got $key => ".var_export($row, true));
|
||||
$count++;
|
||||
}
|
||||
self::assertSame(3, $count);
|
||||
}
|
||||
|
||||
function testUsage() {
|
||||
msg::section("all");
|
||||
$rows = cache::all($this->gendata(),"gendata");
|
||||
$this->_testRows($rows);
|
||||
|
||||
msg::section("get");
|
||||
$rows = cache::get(function() {
|
||||
return self::DATA;
|
||||
},"gendata");
|
||||
$this->_testRows($rows);
|
||||
}
|
||||
|
||||
function testNc() {
|
||||
cache::nc();
|
||||
|
||||
msg::section("first pass");
|
||||
$rows = cache::all($this->gendata(),"gendata");
|
||||
$this->_testRows($rows);
|
||||
|
||||
msg::section("second pass");
|
||||
$rows = cache::all($this->gendata(),"gendata");
|
||||
$this->_testRows($rows);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user