maj nur/sery

This commit is contained in:
Jephté Clain 2024-07-19 16:38:19 +04:00
parent 73432809ad
commit d68cf2b052
134 changed files with 7175 additions and 1695 deletions

View File

@ -18,7 +18,7 @@ class A {
if (is_array($array)) return true; if (is_array($array)) return true;
if ($array instanceof IArrayWrapper) $array = $array->wrappedArray(); if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
if ($array === null || $array === false) $array = []; if ($array === null || $array === false) $array = [];
elseif ($array instanceof Traversable) $array = iterator_to_array($array); elseif ($array instanceof Traversable) $array = cl::all($array);
else $array = [$array]; else $array = [$array];
return false; return false;
} }
@ -28,12 +28,189 @@ class A {
* $array n'a pas été modifié (s'il était déjà un array ou s'il valait null). * $array n'a pas été modifié (s'il était déjà un array ou s'il valait null).
*/ */
static final function ensure_narray(&$array): bool { static final function ensure_narray(&$array): bool {
if ($array === null || is_array($array)) return true;
if ($array instanceof IArrayWrapper) $array = $array->wrappedArray(); if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
if ($array === null || is_array($array)) return true;
if ($array === false) $array = []; if ($array === false) $array = [];
elseif ($array instanceof Traversable) $array = iterator_to_array($array); elseif ($array instanceof Traversable) $array = cl::all($array);
else $array = [$array]; else $array = [$array];
return false; return false;
} }
/**
* s'assurer que $array est un tableau de $size éléments, en complétant avec
* des occurrences de $default si nécessaire
*
* @return bool true si le tableau a été modifié, false sinon
*/
static final function ensure_size(?array &$array, int $size, $default=null): bool {
$modified = false;
if ($array === null) {
$array = [];
$modified = true;
}
if ($size < 0) return $modified;
$count = count($array);
if ($count == $size) return $modified;
if ($count < $size) {
# agrandir le tableau
while ($count++ < $size) {
$array[] = $default;
}
return true;
}
# rétrécir le tableau
$tmparray = [];
foreach ($array as $key => $value) {
if ($size-- == 0) break;
$tmparray[$key] = $value;
}
$array = $tmparray;
return true;
}
static function merge(&$dest, ...$merges): void {
self::ensure_narray($dest);
$dest = cl::merge($dest, ...$merges);
}
static function merge2(&$dest, ...$merges): void {
self::ensure_narray($dest);
$dest = cl::merge2($dest, ...$merges);
}
static final function select(&$dest, ?array $mappings, bool $inverse=false): void {
self::ensure_narray($dest);
$dest = cl::select($dest, $mappings, $inverse);
}
static final function selectm(&$dest, ?array $mappings, ?array $merge=null): void {
self::ensure_narray($dest);
$dest = cl::selectm($dest, $mappings, $merge);
}
static final function mselect(&$dest, ?array $merge, ?array $mappings): void {
self::ensure_narray($dest);
$dest = cl::mselect($dest, $merge, $mappings);
}
static final function pselect(&$dest, ?array $pkeys): void {
self::ensure_narray($dest);
$dest = cl::pselect($dest, $pkeys);
}
static final function pselectm(&$dest, ?array $pkeys, ?array $merge=null): void {
self::ensure_narray($dest);
$dest = cl::pselectm($dest, $pkeys, $merge);
}
static final function mpselect(&$dest, ?array $merge, ?array $pkeys): void {
self::ensure_narray($dest);
$dest = cl::mpselect($dest, $merge, $pkeys);
}
static final function set_nn(&$dest, $key, $value) {
self::ensure_narray($dest);
if ($value !== null) {
if ($key === null) $dest[] = $value;
else $dest[$key] = $value;
}
return $value;
}
static final function append_nn(&$dest, $value) {
return self::set_nn($dest, null, $value);
}
static final function set_nz(&$dest, $key, $value) {
self::ensure_narray($dest);
if ($value !== null && $value !== false) {
if ($key === null) $dest[] = $value;
else $dest[$key] = $value;
}
return $value;
}
static final function append_nz(&$dest, $value) {
self::ensure_narray($dest);
return self::set_nz($dest, null, $value);
}
static final function prepend_nn(&$dest, $value) {
self::ensure_narray($dest);
if ($value !== null) {
if ($dest === null) $dest = [];
array_unshift($dest, $value);
}
return $value;
}
static final function prepend_nz(&$dest, $value) {
self::ensure_narray($dest);
if ($value !== null && $value !== false) {
if ($dest === null) $dest = [];
array_unshift($dest, $value);
}
return $value;
}
static final function replace_nx(&$dest, $key, $value) {
self::ensure_narray($dest);
if ($dest !== null && !array_key_exists($key, $dest)) {
return $dest[$key] = $value;
} else {
return $dest[$key] ?? null;
}
}
static final function replace_n(&$dest, $key, $value) {
self::ensure_narray($dest);
$pvalue = $dest[$key] ?? null;
if ($pvalue === null) $dest[$key] = $value;
return $pvalue;
}
static final function replace_z(&$dest, $key, $value) {
self::ensure_narray($dest);
$pvalue = $dest[$key] ?? null;
if ($pvalue === null || $pvalue === false) $dest[$key] = $value;
return $pvalue;
}
static final function pop(&$dest, $key, $default=null) {
if ($dest === null) return $default;
self::ensure_narray($dest);
if ($key === null) return array_pop($dest);
$value = $dest[$key] ?? $default;
unset($dest[$key]);
return $value;
}
static final function popx(&$dest, ?array $keys): array {
$values = [];
if ($dest === null) return $values;
self::ensure_narray($dest);
if ($keys === null) return $values;
foreach ($keys as $key) {
$values[$key] = self::pop($dest, $key);
}
return $values;
}
static final function filter_if(&$dest, callable $cond): void {
self::ensure_narray($dest);
$dest = cl::filter_if($dest, $cond);
}
static final function filter_z($dest): void { self::filter_if($dest, [cv::class, "z"]);}
static final function filter_nz($dest): void { self::filter_if($dest, [cv::class, "nz"]);}
static final function filter_n($dest): void { self::filter_if($dest, [cv::class, "n"]);}
static final function filter_nn($dest): void { self::filter_if($dest, [cv::class, "nn"]);}
static final function filter_t($dest): void { self::filter_if($dest, [cv::class, "t"]);}
static final function filter_f($dest): void { self::filter_if($dest, [cv::class, "f"]);}
static final function filter_pt($dest): void { self::filter_if($dest, [cv::class, "pt"]);}
static final function filter_pf($dest): void { self::filter_if($dest, [cv::class, "pf"]);}
static final function filter_equals($dest, $value): void { self::filter_if($dest, cv::equals($value)); }
static final function filter_not_equals($dest, $value): void { self::filter_if($dest, cv::not_equals($value)); }
static final function filter_same($dest, $value): void { self::filter_if($dest, cv::same($value)); }
static final function filter_not_same($dest, $value): void { self::filter_if($dest, cv::not_same($value)); }
} }

View File

@ -1,22 +1,31 @@
<?php <?php
namespace nulib; namespace nulib;
use Error;
use Throwable; use Throwable;
/** /**
* Class ExitException: une exception qui indique que l'application souhaite * Class ExitException: une exception qui indique que l'application souhaite
* quitter normalement, avec éventuellement un code d'erreur. * quitter normalement, avec éventuellement un code d'erreur.
*/ */
class ExitException extends UserException { class ExitError extends Error {
function __construct(int $exitcode=0, $userMessage=null, Throwable $previous=null) { function __construct(int $exitcode=0, $userMessage=null, Throwable $previous=null) {
parent::__construct($userMessage, null, $exitcode, $previous); parent::__construct(null, $exitcode, $previous);
$this->userMessage = $userMessage;
} }
function isError(): bool { function isError(): bool {
return $this->getCode() !== 0; return $this->getCode() !== 0;
} }
/** @var ?string */
protected $userMessage;
function haveMessage(): bool { function haveMessage(): bool {
return $this->getUserMessage() !== null; return $this->userMessage !== null;
}
function getUserMessage(): ?string {
return $this->userMessage;
} }
} }

View File

@ -1,5 +1,5 @@
<?php <?php
namespace nulib\file\app; namespace nulib\app;
use nulib\cl; use nulib\cl;
use nulib\file\SharedFile; use nulib\file\SharedFile;
@ -51,11 +51,13 @@ class LockFile {
return $data["locked"]; return $data["locked"];
} }
function warnIfLocked(?array $data=null): void { function warnIfLocked(?array $data=null): bool {
if ($data === null) $data = $this->read(); if ($data === null) $data = $this->read();
if ($data["locked"]) { if ($data["locked"]) {
msg::warning("$data[name]: possède le verrou depuis $data[date_lock] -- $data[title]"); msg::warning("$data[name]: possède le verrou depuis $data[date_lock] -- $data[title]");
return true;
} }
return false;
} }
function lock(?array &$data=null): bool { function lock(?array &$data=null): bool {

354
php/src/app/RunFile.php Normal file
View File

@ -0,0 +1,354 @@
<?php
namespace nulib\app;
use nulib\cl;
use nulib\file\SharedFile;
use nulib\os\path;
use nulib\output\msg;
use nulib\php\time\DateTime;
use nulib\str;
/**
* Class RunFile: une classe permettant de suivre le fonctionnement d'une
* application qui tourne en tâche de fond
*/
class RunFile {
const RUN_EXT = ".run";
const LOCK_EXT = ".lock";
const NAME = null;
function __construct(?string $name, string $file, ?string $logfile=null) {
$file = path::ensure_ext($file, self::RUN_EXT);
$this->name = $name ?? static::NAME;
$this->file = new SharedFile($file);
$this->logfile = $logfile;
}
protected ?string $name;
protected SharedFile $file;
protected ?string $logfile;
function getLogfile(): ?string {
return $this->logfile;
}
protected static function merge(array $data, array $merge): array {
return cl::merge($data, [
"serial" => $data["serial"] + 1,
], $merge);
}
protected function initData(bool $forStart=true): array {
if ($forStart) {
$pid = posix_getpid();
$dateStart = new DateTime();
} else {
$pid = $dateStart = null;
}
return [
"name" => $this->name,
"id" => bin2hex(random_bytes(16)),
"pg_pid" => null,
"pid" => $pid,
"serial" => 0,
# lock
"locked" => false,
"date_lock" => null,
"date_release" => null,
# run
"logfile" => $this->logfile,
"date_start" => $dateStart,
"date_stop" => null,
"exitcode" => null,
"is_done" => null,
# action
"action" => null,
"action_date_start" => null,
"action_current_step" => null,
"action_max_step" => null,
"action_date_step" => null,
];
}
function read(): array {
$data = $this->file->unserialize();
if (!is_array($data)) $data = $this->initData(false);
return $data;
}
protected function willWrite(): array {
$file = $this->file;
$file->lockWrite();
$data = $file->unserialize(null, false, true);
if (!is_array($data)) {
$data = $this->initData(false);
$file->ftruncate();
$file->serialize($data, false, true);
}
return [$file, $data];
}
protected function serialize(SharedFile $file, array $data, ?array $merge=null): void {
$file->ftruncate();
$file->serialize(self::merge($data, $merge), true, true);
}
protected function update(callable $func): void {
/** @var SharedFile$file */
[$file, $data] = $this->willWrite();
$merge = call_user_func($func, $data);
if ($merge !== null && $merge !== false) {
$this->serialize($file, $data, $merge);
} else {
$file->cancelWrite();
}
}
function haveWorked(int $serial, ?int &$currentSerial=null, ?array $data=null): bool {
$data ??= $this->read();
$currentSerial = $data["serial"];
return $serial !== $currentSerial;
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# verrouillage par défaut
function isLocked(?array &$data=null): bool {
$data = $this->read();
return $data["locked"];
}
function warnIfLocked(?array $data=null): bool {
$data ??= $this->read();
if ($data["locked"]) {
msg::warning("$data[name]: possède le verrou depuis $data[date_lock]");
return true;
}
return false;
}
function lock(): bool {
$this->update(function ($data) use (&$locked) {
if ($data["locked"]) {
$locked = false;
return null;
} else {
$locked = true;
return [
"locked" => true,
"date_lock" => new DateTime(),
"date_release" => null,
];
}
});
return $locked;
}
function release(): void {
$this->update(function ($data) {
return [
"locked" => false,
"date_release" => new DateTime(),
];
});
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# cycle de vie de l'application
/**
* indiquer que l'application démarre. l'état est entièrement réinitialisé,
* sauf le PID du leader qui est laissé en l'état
*/
function wfStart(): void {
$this->update(function (array $data) {
return cl::merge($this->initData(), [
"pg_pid" => $data["pg_pid"],
]);
});
}
/** tester si l'application a déjà été démarrée au moins une fois */
function wasStarted(?array $data=null): bool {
$data ??= $this->read();
return $data["date_start"] !== null;
}
/** tester si l'application est démarrée et non arrêtée */
function isStarted(?array $data=null): bool {
$data ??= $this->read();
return $data["date_start"] !== null && $data["date_stop"] === null;
}
/**
* vérifier si l'application marquée comme démarrée tourne réellement
*/
function isRunning(?array $data=null): bool {
$data ??= $this->read();
if ($data["date_start"] === null) return false;
if ($data["date_stop"] !== null) return false;
if (!posix_kill($data["pid"], 0)) {
switch (posix_get_last_error()) {
case 1: #PCNTL_EPERM:
# process auquel on n'a pas accès?! est-ce un autre process qui a
# réutilisé le PID?
return false;
case 3: #PCNTL_ESRCH:
# process inexistant
return false;
case 22: #PCNTL_EINVAL:
# ne devrait pas se produire
return false;
}
}
# process existant auquel on a accès
return true;
}
/** indiquer que l'application s'arrête */
function wfStop(): void {
$this->update(function (array $data) {
return ["date_stop" => new DateTime()];
});
}
/** tester si l'application est déjà été stoppée au moins une fois */
function wasStopped(?array $data=null): bool {
$data ??= $this->read();
return $data["date_stop"] !== null;
}
/** tester si l'application a été démarrée puis arrêtée */
function isStopped(?array $data=null): bool {
$data ??= $this->read();
return $data["date_start"] !== null && $data["date_stop"] !== null;
}
/** après l'arrêt de l'application, mettre à jour le code de retour */
function wfStopped(int $exitcode): void {
$this->update(function (array $data) use ($exitcode) {
return [
"pg_pid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $exitcode,
];
});
}
/**
* comme {@link self::isStopped()} mais ne renvoie true qu'une seule fois si
* $updateDone==true
*/
function isDone(?array &$data=null, bool $updateDone=true): bool {
$done = false;
$this->update(function (array $ldata) use (&$done, &$data, $updateDone) {
$data = $ldata;
if ($data["date_start"] === null || $data["date_stop"] === null || $data["is_done"]) {
return false;
}
$done = true;
if ($updateDone) return ["is_done" => $done];
else return null;
});
return $done;
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# gestion des actions
/** indiquer le début d'une action */
function action(?string $title, ?int $maxSteps=null): void {
$this->update(function (array $data) use ($title, $maxSteps) {
return [
"action" => $title,
"action_date_start" => new DateTime(),
"action_max_step" => $maxSteps,
"action_current_step" => 0,
];
});
}
/** indiquer qu'une étape est franchie dans l'action en cours */
function step(int $nbSteps=1): void {
$this->update(function (array $data) use ($nbSteps) {
return [
"action_date_step" => new DateTime(),
"action_current_step" => $data["action_current_step"] + $nbSteps,
];
});
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Divers
function getLockFile(?string $name=null, ?string $title=null): LockFile {
$ext = self::LOCK_EXT;
if ($name !== null) $ext = ".$name$ext";
$file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT);
$name = str::join("/", [$this->name, $name]);
return new LockFile($file, $name, $title);
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Gestionnaire de tâches (tm_*)
/** démarrer un groupe de process dont le process courant est le leader */
function tm_startPg(): void {
$this->update(function (array $data) {
posix_setsid();
return [
"pg_pid" => posix_getpid(),
];
});
}
/**
* vérifier si on est dans le cas la tâche devrait tourner mais en réalité
* ce n'est pas le cas
*/
function tm_isUndead(?int $pid=null): bool {
$data = $this->read();
if ($data["date_start"] === null) return false;
if ($data["date_stop"] !== null) return false;
$pid ??= $data["pid"];
if (!posix_kill($pid, 0)) {
switch (posix_get_last_error()) {
case 1: #PCNTL_EPERM:
# process auquel on n'a pas accès?! est-ce un autre process qui a
# réutilisé le PID?
return false;
case 3: #PCNTL_ESRCH:
# process inexistant
return true;
case 22: #PCNTL_EINVAL:
# ne devrait pas se produire
return false;
}
}
# process existant auquel on a accès
return false;
}
function tm_isReapable(): bool {
$data = $this->read();
return $data["date_stop"] !== null && $data["exitcode"] === null;
}
/** marquer la tâche comme terminée */
function tm_reap(?int $pid=null): void {
$data = $this->read();
$pid ??= $data["pid"];
pcntl_waitpid($pid, $status);
$exitcode = pcntl_wifexited($status)? pcntl_wexitstatus($status): 127;
$this->update(function (array $data) use ($exitcode) {
return [
"pg_pid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $data["exitcode"] ?? $exitcode,
];
});
}
}

136
php/src/app/launcher.php Normal file
View File

@ -0,0 +1,136 @@
<?php
namespace nulib\app;
use nulib\cl;
use nulib\file\TmpfileWriter;
use nulib\os\path;
use nulib\os\proc\Cmd;
use nulib\output\msg;
use nulib\StateException;
use nulib\str;
use nulib\wip\app\app;
class launcher {
/**
* transformer une liste d'argument de la forme
* - ["myArg" => $value] devient ["--my-arg", "$value"]
* - ["myOpt" => true] devient ["--my-opt"]
* - ["myOpt" => false] est momis
* - les valeurs séquentielles sont prises telles quelles
*/
static function verifix_args(array $args): array {
if (!cl::is_list($args)) {
$fixedArgs = [];
$index = 0;
foreach ($args as $arg => $value) {
if ($arg === $index) {
$index++;
$fixedArgs[] = $value;
continue;
} elseif ($value === false) {
continue;
}
$arg = str::us2camel($arg);
$arg = str::camel2us($arg, false, "-");
$arg = str_replace("_", "-", $arg);
$fixedArgs[] = "--$arg";
if ($value !== true) $fixedArgs[] = "$value";
}
$args = $fixedArgs;
}
# corriger le chemin de l'application pour qu'il soit absolu et normalisé
$args[0] = path::abspath($args[0]);
return $args;
}
static function launch(string $appClass, array $args): int {
$app = app::get();
$vendorBindir = $app->getVendorbindir();
$launch_php = "$vendorBindir/_launch.php";
if (!file_exists($launch_php)) {
$launch_php = __DIR__."/../../lib/_launch.php";
}
$tmpfile = new TmpfileWriter();
$tmpfile->keep()->serialize($app->getParams());
$args = self::verifix_args($args);
$cmd = new Cmd([
$launch_php,
"--internal-use", $tmpfile->getFile(),
$appClass, "--", ...$args,
]);
$cmd->addRedir("both", "/tmp/nulib_app_launcher-launch.log");
$cmd->passthru($exitcode);
# attendre un peu que la commande aie le temps de s'initialiser
sleep(1);
$tmpfile->close();
return $exitcode;
}
static function _start(array $args, Runfile $runfile): bool {
if ($runfile->warnIfLocked()) return false;
$pid = pcntl_fork();
if ($pid == -1) {
# parent, impossible de forker
throw new StateException("unable to fork");
} elseif ($pid) {
# parent, fork ok
return true;
} else {
## child, fork ok
# Créer un groupe de process, pour pouvoir tuer tous les enfants en même temps
$runfile->tm_startPg();
$logfile = $runfile->getLogfile() ?? "/tmp/nulib_app_launcher-_start.log";
$pid = posix_getpid();
$exitcode = -776;
try {
# puis lancer la commande
$cmd = new Cmd($args);
$cmd->addSource("/g/init.env");
$cmd->addRedir("both", $logfile, true);
msg::debug("$pid: launching\n".$cmd->getCmd());
$cmd->fork_exec($exitcode);
msg::debug("$pid: exitcode=$exitcode");
return true;
} finally {
$runfile->wfStopped($exitcode);
}
}
}
static function _stop(Runfile $runfile): void {
$data = $runfile->read();
$pid = $data["pg_pid"];
if ($pid === null) {
msg::warning("$data[name]: groupe de process inconnu");
return;
}
msg::action("kill $pid");
if (!posix_kill(-$pid, SIGKILL)) {
switch (posix_get_last_error()) {
case PCNTL_ESRCH:
msg::afailure("process inexistant");
break;
case PCNTL_EPERM:
msg::afailure("process non accessible");
break;
case PCNTL_EINVAL:
msg::afailure("signal invalide");
break;
}
return;
}
$timeout = 10;
while ($runfile->tm_isUndead($pid)) {
sleep(1);
if (--$timeout == 0) {
msg::afailure("impossible d'arrêter la tâche");
return;
}
}
$runfile->wfStopped(-778);
msg::asuccess();
}
}

View File

@ -2,6 +2,7 @@
namespace nulib; namespace nulib;
use ArrayAccess; use ArrayAccess;
use nulib\php\func;
use Traversable; use Traversable;
/** /**
@ -12,19 +13,73 @@ use Traversable;
* pour retourner un nouveau tableau * pour retourner un nouveau tableau
*/ */
class cl { class cl {
/**
* retourner un array avec les éléments retournés par l'itérateur. les clés
* numériques sont réordonnées, les clés chaine sont laissées en l'état
*/
static final function all(?iterable $iterable): array {
if ($iterable === null) return [];
if (is_array($iterable)) return $iterable;
$array = [];
foreach ($iterable as $key => $value) {
if (is_int($key)) $array[] = $value;
else $array[$key] = $value;
}
return $array;
}
/**
* retourner la première valeur de $array ou $default si le tableau est null
* ou vide
*/
static final function first(?iterable $iterable, $default=null) {
if (is_array($iterable)) {
$key = array_key_first($iterable);
if ($key === null) return $default;
return $iterable[$key];
}
if (is_iterable($iterable)) {
foreach ($iterable as $value) {
return $value;
}
}
return $default;
}
/**
* retourner la dernière valeur de $array ou $default si le tableau est null
* ou vide
*/
static final function last(?iterable $iterable, $default=null) {
if (is_array($iterable)) {
$key = array_key_last($iterable);
if ($key === null) return $default;
return $iterable[$key];
}
$value = $default;
if (is_iterable($iterable)) {
foreach ($iterable as $value) {
# parcourir tout l'iterateur pour avoir le dernier élément
}
}
return $value;
}
/** retourner un array non null à partir de $array */ /** retourner un array non null à partir de $array */
static final function with($array): array { static final function with($array): array {
if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
if (is_array($array)) return $array; if (is_array($array)) return $array;
elseif ($array === null || $array === false) return []; elseif ($array === null || $array === false) return [];
elseif ($array instanceof Traversable) return iterator_to_array($array); elseif ($array instanceof Traversable) return self::all($array);
else return [$array]; else return [$array];
} }
/** retourner un array à partir de $array, ou null */ /** retourner un array à partir de $array, ou null */
static final function withn($array): ?array { static final function withn($array): ?array {
if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
if (is_array($array)) return $array; if (is_array($array)) return $array;
elseif ($array === null || $array === false) return null; elseif ($array === null || $array === false) return null;
elseif ($array instanceof Traversable) return iterator_to_array($array); elseif ($array instanceof Traversable) return self::all($array);
else return [$array]; else return [$array];
} }
@ -82,6 +137,128 @@ class cl {
return $default; return $default;
} }
/**
* retourner un tableau construit à partir des clés de $keys
* - [$to => $from] --> $dest[$to] = self::get($array, $from)
* - [$to => null] --> $dest[$to] = null
* - [$to => false] --> NOP
* - [$to] --> $dest[$to] = self::get($array, $to)
* - [null] --> $dest[] = null
* - [false] --> NOP
*
* Si $inverse===true, le mapping est inversé:
* - [$to => $from] --> $dest[$from] = self::get($array, $to)
* - [$to => null] --> $dest[$to] = self::get($array, $to)
* - [$to => false] --> NOP
* - [$to] --> $dest[$to] = self::get($array, $to)
* - [null] --> NOP (XXX que faire dans ce cas?)
* - [false] --> NOP
*
* notez que l'ordre est inversé par rapport à {@link self::rekey()} qui
* attend des mappings [$from => $to], alors que cette méthode attend des
* mappings [$to => $from]
*/
static final function select($array, ?array $mappings, bool $inverse=false): array {
$dest = [];
$index = 0;
if (!$inverse) {
foreach ($mappings as $to => $from) {
if ($to === $index) {
$index++;
$to = $from;
if ($to === false) continue;
elseif ($to === null) $dest[] = null;
else $dest[$to] = self::get($array, $to);
} elseif ($from === false) {
continue;
} elseif ($from === null) {
$dest[$to] = null;
} else {
$dest[$to] = self::get($array, $from);
}
}
} else {
foreach ($mappings as $to => $from) {
if ($to === $index) {
$index++;
$to = $from;
if ($to === false) continue;
elseif ($to === null) continue;
else $dest[$to] = self::get($array, $to);
} elseif ($from === false) {
continue;
} elseif ($from === null) {
$dest[$to] = self::get($array, $to);
} else {
$dest[$from] = self::get($array, $to);
}
}
}
return $dest;
}
/**
* obtenir la liste des clés finalement obtenues après l'appel à
* {@link self::select()} avec le mapping spécifié
*/
static final function selected_keys(?array $mappings): array {
if ($mappings === null) return [];
$keys = [];
$index = 0;
foreach ($mappings as $to => $from) {
if ($to === $index) {
if ($from === false) continue;
elseif ($from === null) $keys[] = $index;
else $keys[] = $from;
$index++;
} elseif ($from === false) {
continue;
} else {
$keys[] = $to;
}
}
return $keys;
}
/**
* méthode de convenance qui sélectionne certaines clés de $array avec
* {@link self::select()} puis merge le tableau $merge au résultat.
*/
static final function selectm($array, ?array $mappings, ?array $merge=null): array {
return cl::merge(self::select($array, $mappings), $merge);
}
/**
* méthode de convenance qui merge $merge dans $array puis sélectionne
* certaines clés avec {@link self::select()}
*/
static final function mselect($array, ?array $merge, ?array $mappings): array {
return self::select(cl::merge($array, $merge), $mappings);
}
/**
* construire un sous-ensemble du tableau $array en sélectionnant les clés de
* $includes qui ne sont pas mentionnées dans $excludes.
*
* - si $includes===null && $excludes===null, retourner le tableau inchangé
* - si $includes vaut null, prendre toutes les clés
*
*/
static final function xselect($array, ?array $includes, ?array $excludes=null): ?array {
if ($array === null) return null;
$array = self::withn($array);
if ($includes === null && $excludes === null) return $array;
if ($includes === null) $includes = array_keys($array);
if ($excludes === null) $excludes = [];
$result = [];
foreach ($array as $key => $value) {
if (!in_array($key, $includes)) continue;
if (in_array($key, $excludes)) continue;
$result[$key] = $value;
}
return $result;
}
/** /**
* si $array est un array ou une instance de ArrayAccess, créer ou modifier * si $array est un array ou une instance de ArrayAccess, créer ou modifier
* l'élément dont la clé est $key * l'élément dont la clé est $key
@ -121,19 +298,11 @@ class cl {
return $array !== null? array_keys($array): []; return $array !== null? array_keys($array): [];
} }
/**
* retourner la première valeur de $array ou $default si le tableau est null
* ou vide
*/
static final function first($array, $default=null) {
if (is_array($array)) return $array[array_key_first($array)];
return $default;
}
############################################################################# #############################################################################
/** /**
* Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées. * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées.
* IMPORTANT: les clés numériques sont réordonnées.
* Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null. * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null.
*/ */
static final function merge(...$arrays): ?array { static final function merge(...$arrays): ?array {
@ -145,6 +314,34 @@ class cl {
return $merges? array_merge(...$merges): null; return $merges? array_merge(...$merges): null;
} }
/**
* Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées.
* IMPORTANT: les clés numériques NE SONT PAS réordonnées.
* Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null.
*/
static final function merge2(...$arrays): ?array {
$merged = null;
foreach ($arrays as $array) {
foreach (self::with($array) as $key => $value) {
$merged[$key] = $value;
}
}
return $merged;
}
#############################################################################
static final function map(callable $callback, ?iterable $array): array {
$result = [];
if ($array !== null) {
$ctx = func::_prepare($callback);
foreach ($array as $key => $value) {
$result[$key] = func::_call($ctx, [$value, $key]);
}
}
return $result;
}
############################################################################# #############################################################################
/** /**
@ -236,6 +433,52 @@ class cl {
return $result; return $result;
} }
/**
* retourner un tableau construit à partir des chemins de clé de $pkeys
* ces chemins peuvent être exprimés de plusieurs façon:
* - [$key => $pkey] --> $dest[$key] = self::pget($array, $pkey)
* - [$key => null] --> $dest[$key] = null
* - [$pkey] --> $dest[$key] = self::pget($array, $pkey)
* avec $key = implode("__", $pkey))
* - [null] --> $dest[] = null
* - [false] --> NOP
*/
static final function pselect($array, ?array $pkeys): array {
$dest = [];
$index = 0;
foreach ($pkeys as $key => $pkey) {
if ($key === $index) {
$index++;
if ($pkey === null) continue;
$value = self::pget($array, $pkey);
if (!is_array($pkey)) $pkey = explode(".", strval($pkey));
$key = implode("__", $pkey);
} elseif ($pkey === null) {
$value = null;
} else {
$value = self::pget($array, $pkey);
}
$dest[$key] = $value;
}
return $dest;
}
/**
* méthode de convenance qui sélectionne certaines clés de $array avec
* {@link self::pselect()} puis merge le tableau $merge au résultat.
*/
static final function pselectm($array, ?array $pkeys, ?array $merge=null): array {
return cl::merge(self::pselect($array, $pkeys), $merge);
}
/**
* méthode de convenance qui merge $merge dans $array puis sélectionne
* certaines clés avec {@link self::pselect()}
*/
static final function mpselect($array, ?array $merge, ?array $mappings): array {
return self::pselect(cl::merge($array, $merge), $mappings);
}
/** /**
* modifier la valeur au chemin de clé $keys dans le tableau $array * modifier la valeur au chemin de clé $keys dans le tableau $array
* *
@ -353,9 +596,12 @@ class cl {
/** /**
* retourner le tableau $array en "renommant" les clés selon le tableau * retourner le tableau $array en "renommant" les clés selon le tableau
* $mappings qui contient des associations de la forme [$from => $to] * $mappings qui contient des associations de la forme [$from => $to]
*
* Si $inverse===true, renommer dans le sens $to => $from
*/ */
static function rekey(?array $array, ?array $mappings): ?array { static function rekey(?array $array, ?array $mappings, bool $inverse=false): ?array {
if ($array === null || $mappings === null) return $array; if ($array === null || $mappings === null) return $array;
if ($inverse) $mappings = array_flip($mappings);
$mapped = []; $mapped = [];
foreach ($array as $key => $value) { foreach ($array as $key => $value) {
if (array_key_exists($key, $mappings)) $key = $mappings[$key]; if (array_key_exists($key, $mappings)) $key = $mappings[$key];
@ -364,6 +610,19 @@ class cl {
return $mapped; return $mapped;
} }
/**
* indiquer si {@link self::rekey()} modifierai le tableau indiqué (s'il y a
* des modifications à faire)
*/
static function would_rekey(?array $array, ?array $mappings, bool $inverse=false): bool {
if ($array === null || $mappings === null) return false;
if ($inverse) $mappings = array_flip($mappings);
foreach ($array as $key => $value) {
if (array_key_exists($key, $mappings)) return true;
}
return false;
}
############################################################################# #############################################################################
/** tester si tous les éléments du tableau satisfont la condition */ /** tester si tous les éléments du tableau satisfont la condition */

View File

@ -1,23 +1,129 @@
<?php <?php
namespace nulib\db; namespace nulib\db;
use nulib\php\func;
use nulib\ValueException;
use Traversable;
/** /**
* Class Capacitor: un objet permettant d'attaquer un canal spécique d'une * Class Capacitor: un objet permettant d'attaquer un canal spécifique d'une
* instance de {@link CapacitorStorage} * instance de {@link CapacitorStorage}
*/ */
class Capacitor { class Capacitor implements ITransactor {
function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) { function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) {
$this->storage = $storage; $this->storage = $storage;
$this->channel = $channel; $this->channel = $channel;
$this->channel->setCapacitor($this);
if ($ensureExists) $this->ensureExists(); if ($ensureExists) $this->ensureExists();
} }
/** @var CapacitorStorage */ /** @var CapacitorStorage */
protected $storage; protected $storage;
function getStorage(): CapacitorStorage {
return $this->storage;
}
function db(): IDatabase {
return $this->getStorage()->db();
}
/** @var CapacitorChannel */ /** @var CapacitorChannel */
protected $channel; protected $channel;
function getChannel(): CapacitorChannel {
return $this->channel;
}
function getTableName(): string {
return $this->getChannel()->getTableName();
}
/** @var CapacitorChannel[] */
protected ?array $subChannels = null;
protected ?array $subManageTransactions = null;
function willUpdate(...$channels): self {
if ($this->subChannels === null) {
# désactiver la gestion des transaction sur le channel local aussi
$this->subChannels[] = $this->channel;
}
if ($channels) {
foreach ($channels as $channel) {
if ($channel instanceof Capacitor) $channel = $channel->getChannel();
if ($channel instanceof CapacitorChannel) {
$this->subChannels[] = $channel;
} else {
throw ValueException::invalid_type($channel, CapacitorChannel::class);
}
}
}
return $this;
}
function inTransaction(): bool {
return $this->db()->inTransaction();
}
function beginTransaction(?callable $func=null, bool $commit=true): void {
$db = $this->db();
if ($this->subChannels !== null) {
# on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait
if ($this->subManageTransactions === null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$this->subManageTransactions ??= [];
if (!array_key_exists($name, $this->subManageTransactions)) {
$this->subManageTransactions[$name] = $channel->isManageTransactions();
}
$channel->setManageTransactions(false);
}
if (!$db->inTransaction()) $db->beginTransaction();
}
} elseif (!$db->inTransaction()) {
$db->beginTransaction();
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
}
} finally {
if ($commit && !$commited) $this->rollback();
}
}
}
protected function beforeEndTransaction(): void {
if ($this->subManageTransactions !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$channel->setManageTransactions($this->subManageTransactions[$name]);
}
$this->subManageTransactions = null;
}
}
function commit(): void {
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $db->commit();
}
function rollback(): void {
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $db->rollback();
}
function getCreateSql(): string {
return $this->storage->_getCreateSql($this->channel);
}
function exists(): bool { function exists(): bool {
return $this->storage->_exists($this->channel); return $this->storage->_exists($this->channel);
} }
@ -26,15 +132,16 @@ class Capacitor {
$this->storage->_ensureExists($this->channel); $this->storage->_ensureExists($this->channel);
} }
function reset(): void { function reset(bool $recreate=false): void {
$this->storage->_reset($this->channel); $this->storage->_reset($this->channel, $recreate);
} }
function charge($item, ?callable $func=null, ?array $args=null): int { function charge($item, $func=null, ?array $args=null, ?array &$values=null): int {
return $this->storage->_charge($this->channel, $item, $func, $args); if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_charge($this->channel, $item, $func, $args, $values);
} }
function discharge($filter=null, ?bool $reset=null): iterable { function discharge(bool $reset=true): Traversable {
return $this->storage->_discharge($this->channel, $reset); return $this->storage->_discharge($this->channel, $reset);
} }
@ -42,16 +149,22 @@ class Capacitor {
return $this->storage->_count($this->channel, $filter); return $this->storage->_count($this->channel, $filter);
} }
function one($filter): ?array { function one($filter, ?array $mergeQuery=null): ?array {
return $this->storage->_one($this->channel, $filter); return $this->storage->_one($this->channel, $filter, $mergeQuery);
} }
function all($filter): iterable { function all($filter, ?array $mergeQuery=null): Traversable {
return $this->storage->_all($this->channel, $filter); return $this->storage->_all($this->channel, $filter, $mergeQuery);
} }
function each($filter, ?callable $func=null, ?array $args=null): int { function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
return $this->storage->_each($this->channel, $filter, $func, $args); if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_each($this->channel, $filter, $func, $args, $mergeQuery, $nbUpdated);
}
function delete($filter, $func=null, ?array $args=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_delete($this->channel, $filter, $func, $args);
} }
function close(): void { function close(): void {

View File

@ -1,48 +1,166 @@
<?php <?php
namespace nulib\db; namespace nulib\db;
use nulib\cl;
use nulib\str;
use Traversable;
/** /**
* Class CapacitorChannel: un canal d'une instance de {@link ICapacitor} * Class CapacitorChannel: un canal d'une instance de {@link ICapacitor}
*/ */
class CapacitorChannel { class CapacitorChannel {
const NAME = null; const NAME = null;
const TABLE_NAME = null;
const COLUMN_DEFINITIONS = null;
const PRIMARY_KEYS = null;
const MANAGE_TRANSACTIONS = true;
const EACH_COMMIT_THRESHOLD = 100; const EACH_COMMIT_THRESHOLD = 100;
static function verifix_name(?string $name): string { const USE_CACHE = false;
if ($name === null) $name = "default";
return strtolower($name); static function verifix_name(?string &$name, ?string &$tableName=null): void {
if ($name !== null) {
$name = strtolower($name);
if ($tableName === null) {
$tableName = str_replace("-", "_", $tableName) . "_channel";
}
} else {
$name = static::class;
if ($name === self::class) {
$name = "default";
if ($tableName === null) $tableName = "default_channel";
} else {
$name = preg_replace('/^.*\\\\/', "", $name);
$name = preg_replace('/Channel$/', "", $name);
$name = lcfirst($name);
if ($tableName === null) $tableName = str::camel2us($name);
$name = strtolower($name);
}
}
} }
function __construct(?string $name=null, ?int $eachCommitThreshold=null) { protected static function verifix_eachCommitThreshold(?int $eachCommitThreshold): ?int {
$this->name = self::verifix_name($name ?? static::NAME); $eachCommitThreshold ??= static::EACH_COMMIT_THRESHOLD;
$this->eachCommitThreshold = $eachCommitThreshold ?? static::EACH_COMMIT_THRESHOLD; if ($eachCommitThreshold < 0) $eachCommitThreshold = null;
return $eachCommitThreshold;
}
function __construct(?string $name=null, ?int $eachCommitThreshold=null, ?bool $manageTransactions=null) {
$name ??= static::NAME;
$tableName ??= static::TABLE_NAME;
self::verifix_name($name, $tableName);
$this->name = $name;
$this->tableName = $tableName;
$this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
$this->useCache = static::USE_CACHE;
$this->setup = false;
$this->created = false; $this->created = false;
$columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS);
$primaryKeys = cl::withn(static::PRIMARY_KEYS);
if ($primaryKeys === null && $columnDefinitions !== null) {
$index = 0;
foreach ($columnDefinitions as $col => $def) {
if ($col === $index) {
$index++;
if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) {
$primaryKeys = preg_split('/\s*,\s*/', trim($ms[1]));
}
} else {
if (preg_match('/\bprimary\s+key\b/i', $def)) {
$primaryKeys[] = $col;
}
}
}
}
$this->columnDefinitions = $columnDefinitions;
$this->primaryKeys = $primaryKeys;
} }
/** @var string */ protected string $name;
protected $name;
function getName(): string { function getName(): string {
return $this->name; return $this->name;
} }
protected string $tableName;
function getTableName(): string {
return $this->tableName;
}
/**
* @var bool indiquer si les modifications de each doivent être gérées dans
* une transaction. si false, l'utilisateur doit lui même gérer la
* transaction.
*/
protected bool $manageTransactions;
function isManageTransactions(): bool {
return $this->manageTransactions;
}
function setManageTransactions(bool $manageTransactions=true): self {
$this->manageTransactions = $manageTransactions;
return $this;
}
/** /**
* @var ?int nombre maximum de modifications dans une transaction avant un * @var ?int nombre maximum de modifications dans une transaction avant un
* commit automatique dans {@link Capacitor::each()}. Utiliser null pour * commit automatique dans {@link Capacitor::each()}. Utiliser null pour
* désactiver la fonctionnalité. * désactiver la fonctionnalité.
*
* ce paramètre n'a d'effet que si $manageTransactions==true
*/ */
protected $eachCommitThreshold; protected ?int $eachCommitThreshold;
function getEachCommitThreshold(): ?int { function getEachCommitThreshold(): ?int {
return $this->eachCommitThreshold; return $this->eachCommitThreshold;
} }
function getTableName(): string { function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
return $this->name."_channel"; $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
return $this;
} }
protected $created; /**
* @var bool faut-il passer par le cache pour les requêtes de all(), each()
* et delete()?
* ça peut être nécessaire avec MySQL/MariaDB si on utilise les requêtes non
* bufférisées, et que la fonction manipule la base de données
*/
protected bool $useCache;
function isUseCache(): bool {
return $this->useCache;
}
function setUseCache(bool $useCache=true): self {
$this->useCache = $useCache;
return $this;
}
/**
* initialiser ce channel avant sa première utilisation.
*/
protected function setup(): void {
}
protected bool $setup;
function ensureSetup() {
if (!$this->setup) {
$this->setup();
$this->setup = true;
}
}
protected bool $created;
function isCreated(): bool { function isCreated(): bool {
return $this->created; return $this->created;
@ -52,13 +170,15 @@ class CapacitorChannel {
$this->created = $created; $this->created = $created;
} }
protected ?array $columnDefinitions;
/** /**
* retourner un ensemble de définitions pour des colonnes supplémentaires à * retourner un ensemble de définitions pour des colonnes supplémentaires à
* insérer lors du chargement d'une valeur * insérer lors du chargement d'une valeur
* *
* la clé primaire "id_" a pour définition "integer primary key autoincrement". * la clé primaire "id_" a pour définition "integer primary key autoincrement".
* elle peut être redéfinie, et dans ce cas la valeur à utiliser doit être * elle peut être redéfinie, et dans ce cas la valeur à utiliser doit être
* retournée par {@link getKeyValues()} * retournée par {@link getItemValues()}
* *
* la colonne "item__" contient la valeur sérialisée de l'élément chargé. bien * la colonne "item__" contient la valeur sérialisée de l'élément chargé. bien
* que ce soit possible techniquement, cette colonne n'a pas à être redéfinie * que ce soit possible techniquement, cette colonne n'a pas à être redéfinie
@ -68,46 +188,143 @@ class CapacitorChannel {
* lors de l'insertion dans la base de données, et automatiquement désérialisées * lors de l'insertion dans la base de données, et automatiquement désérialisées
* avant d'être retournées à l'utilisateur (sans le suffixe "__") * avant d'être retournées à l'utilisateur (sans le suffixe "__")
*/ */
function getKeyDefinitions(): ?array { function getColumnDefinitions(): ?array {
return null; return $this->columnDefinitions;
}
protected ?array $primaryKeys;
function getPrimaryKeys(): ?array {
return $this->primaryKeys;
} }
/** /**
* calculer les valeurs des colonnes supplémentaires à insérer pour le * calculer les valeurs des colonnes supplémentaires à insérer pour le
* chargement de $item * chargement de $item. pour une même valeur de $item, la valeur de retour
* doit toujours être la même. pour rajouter des valeurs supplémentaires qui
* dépendent de l'environnement, il faut plutôt les retournner dans
* {@link self::onCreate()} ou {@link self::onUpdate()}
* *
* Cette méthode est utilisée par {@link Capacitor::charge()}. Si une valeur * Cette méthode est utilisée par {@link Capacitor::charge()}. Si la clé
* "id_" est retourné, la ligne correspondate existante est mise à jour * primaire est incluse (il s'agit généralement de "id_"), la ligne
* correspondate est mise à jour si elle existe.
* Retourner la clé primaire par cette méthode est l'unique moyen de
* déclencher une mise à jour plutôt qu'une nouvelle création.
*
* Retourner [false] pour annuler le chargement
*/ */
function getKeyValues($item): ?array { function getItemValues($item): ?array {
return null; return null;
} }
/** /**
* Avant d'utiliser un id pour rechercher dans la base de donnée, corriger sa * Avant d'utiliser un id pour rechercher dans la base de donnée, corriger sa
* valeur le cas échéant. * valeur le cas échéant.
*
* Cette fonction assume que la clé primaire n'est pas multiple. Elle n'est
* pas utilisée si une clé primaire multiple est définie.
*/ */
function verifixId(string &$id): void { function verifixId(string &$id): void {
} }
/** /**
* méthode appelée lors du chargement d'un élément avec * retourne true si un nouvel élément ou un élément mis à jour a été chargé.
* {@link Capacitor::charge()} * false si l'élément chargé est identique au précédent.
*
* cette méthode doit être utilisée dans {@link self::onUpdate()}
*/
function wasRowModified(array $values, array $pvalues): bool {
return $values["item__sum_"] !== $pvalues["item__sum_"];
}
final function serialize($item): ?string {
return $item !== null? serialize($item): null;
}
final function unserialize(?string $serial) {
return $serial !== null? unserialize($serial): null;
}
const SERIAL_DEFINITION = "mediumtext";
const SUM_DEFINITION = "varchar(40)";
final function sum(?string $serial, $value=null): ?string {
if ($serial === null) $serial = $this->serialize($value);
return $serial !== null? sha1($serial): null;
}
final function isSerialCol(string &$key): bool {
return str::del_suffix($key, "__");
}
final function getSumCols(string $key): array {
return ["${key}__", "${key}__sum_"];
}
function getSum(string $key, $value): array {
$sumCols = $this->getSumCols($key);
$serial = $this->serialize($value);
$sum = $this->sum($serial, $value);
return array_combine($sumCols, [$serial, $sum]);
}
function wasSumModified(string $key, $value, array $pvalues): bool {
$sumCol = $this->getSumCols($key)[1];
$sum = $this->sum(null, $value);
$psum = $pvalues[$sumCol] ?? $this->sum(null, $pvalues[$key] ?? null);
return $sum !== $psum;
}
function _wasSumModified(string $key, array $row, array $prow): bool {
$sumCol = $this->getSumCols($key)[1];
$sum = $row[$sumCol] ?? null;
$psum = $prow[$sumCol] ?? null;
return $sum !== $psum;
}
/**
* méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
* créer un nouvel élément
* *
* @param mixed $item l'élément à charger * @param mixed $item l'élément à charger
* @param array $updates les valeurs calculées par {@link getKeyValues()} * @param array $values la ligne à créer, calculée à partir de $item et des
* @param ?array $row la ligne à mettre à jour. vaut null s'il faut insérer * valeurs retournées par {@link getItemValues()}
* une nouvelle ligne * @return ?array le cas échéant, un tableau non null à merger dans $values et
* @return ?array le cas échéant, un tableau non null à merger dans $updates * utilisé pour provisionner la ligne nouvellement créée.
* et utilisé pour provisionner la ligne nouvellement créée, ou mettre à jour * Retourner [false] pour annuler le chargement (la ligne n'est pas créée)
* la ligne existante
* *
* Si $item est modifié dans cette méthode, il est possible de le retourner * Si $item est modifié dans cette méthode, il est possible de le retourner
* avec la clé "item" pour mettre à jour la ligne correspondante. * avec la clé "item" pour mettre à jour la ligne correspondante.
* La colonne "id_" ne peut pas être modifiée: si "id_" est retourné, il est *
* ignoré * la création ou la mise à jour est uniquement décidée en fonction des
* valeurs calculées par {@link self::getItemValues()}. Bien que cette méthode
* peut techniquement retourner de nouvelles valeurs pour la clé primaire, ça
* risque de créer des doublons
*/ */
function onCharge($item, array $updates, ?array $row): ?array { function onCreate($item, array $values, ?array $alwaysNull): ?array {
return null;
}
/**
* méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
* mettre à jour un élément existant
*
* @param mixed $item l'élément à charger
* @param array $values la nouvelle ligne, calculée à partir de $item et
* des valeurs retournées par {@link getItemValues()}
* @param array $pvalues la précédente ligne, chargée depuis la base de
* données
* @return ?array null s'il ne faut pas mettre à jour la ligne. sinon, ce
* tableau est mergé dans $values puis utilisé pour mettre à jour la ligne
* existante
* Retourner [false] pour annuler le chargement (la ligne n'est pas mise à
* jour)
*
* - Il est possible de mettre à jour $item en le retourant avec la clé "item"
* - La clé primaire (il s'agit généralement de "id_") ne peut pas être
* modifiée. si elle est retournée, elle est ignorée
*/
function onUpdate($item, array $values, array $pvalues): ?array {
return null; return null;
} }
@ -116,16 +333,75 @@ class CapacitorChannel {
* {@link Capacitor::each()} * {@link Capacitor::each()}
* *
* @param mixed $item l'élément courant * @param mixed $item l'élément courant
* @param ?array $row la ligne à mettre à jour. * @param ?array $values la ligne courante
* @return ?array le cas échéant, un tableau non null utilisé pour mettre à * @return ?array le cas échéant, un tableau non null utilisé pour mettre à
* jour la ligne courante * jour la ligne courante
* *
* Si $item est modifié dans cette méthode, il est possible de le retourner * - Il est possible de mettre à jour $item en le retourant avec la clé "item"
* avec la clé "item" pour mettre à jour la ligne correspondante * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
* La colonne "id_" ne peut pas être modifiée: si "id_" est retourné, il est * modifiée. si elle est retournée, elle est ignorée
* ignoré
*/ */
function onEach($item, array $row): ?array { function onEach($item, array $values): ?array {
return null; return null;
} }
const onEach = "->".[self::class, "onEach"][1];
/**
* méthode appelée lors du parcours des éléments avec
* {@link Capacitor::delete()}
*
* @param mixed $item l'élément courant
* @param ?array $values la ligne courante
* @return bool true s'il faut supprimer la ligne, false sinon
*/
function onDelete($item, array $values): bool {
return true;
}
const onDelete = "->".[self::class, "onDelete"][1];
#############################################################################
# Méthodes déléguées pour des workflows centrés sur le channel
/**
* @var Capacitor|null instance de Capacitor par laquelle cette instance est
* utilisée
*/
protected ?Capacitor $capacitor;
function getCapacitor(): ?Capacitor {
return $this->capacitor;
}
function setCapacitor(Capacitor $capacitor): self {
$this->capacitor = $capacitor;
return $this;
}
function charge($item, $func=null, ?array $args=null, ?array &$values=null): int {
return $this->capacitor->charge($item, $func, $args, $values);
}
function discharge(bool $reset=true): Traversable {
return $this->capacitor->discharge($reset);
}
function count($filter=null): int {
return $this->capacitor->count($filter);
}
function one($filter, ?array $mergeQuery=null): ?array {
return $this->capacitor->one($filter, $mergeQuery);
}
function all($filter, ?array $mergeQuery=null): Traversable {
return $this->capacitor->all($filter, $mergeQuery);
}
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated);
}
function delete($filter, $func=null, ?array $args=null): int {
return $this->capacitor->delete($filter, $func, $args);
}
} }

View File

@ -2,104 +2,622 @@
namespace nulib\db; namespace nulib\db;
use nulib\cl; use nulib\cl;
use nulib\db\cache\cache;
use nulib\php\func; use nulib\php\func;
use nulib\ValueException;
use Traversable;
/** /**
* Class CapacitorStorage: objet permettant d'accumuler des données pour les * Class CapacitorStorage: objet permettant d'accumuler des données pour les
* réutiliser plus tard * réutiliser plus tard
*/ */
abstract class CapacitorStorage { abstract class CapacitorStorage {
abstract protected function getChannel(?string $name): CapacitorChannel; abstract function db(): IDatabase;
abstract function _exists(CapacitorChannel $channel): bool; /** @var CapacitorChannel[] */
protected $channels;
function addChannel(CapacitorChannel $channel): CapacitorChannel {
$this->_create($channel);
$this->channels[$channel->getName()] = $channel;
return $channel;
}
protected function getChannel(?string $name): CapacitorChannel {
CapacitorChannel::verifix_name($name);
$channel = $this->channels[$name] ?? null;
if ($channel === null) {
$channel = $this->addChannel(new CapacitorChannel($name));
}
return $channel;
}
/** DOIT être défini dans les classes dérivées */
const PRIMARY_KEY_DEFINITION = null;
const COLUMN_DEFINITIONS = [
"item__" => CapacitorChannel::SERIAL_DEFINITION,
"item__sum_" => CapacitorChannel::SUM_DEFINITION,
"created_" => "datetime",
"modified_" => "datetime",
];
protected function ColumnDefinitions(CapacitorChannel $channel): array {
$definitions = [];
if ($channel->getPrimaryKeys() === null) {
$definitions[] = static::PRIMARY_KEY_DEFINITION;
}
$definitions[] = $channel->getColumnDefinitions();
$definitions[] = static::COLUMN_DEFINITIONS;
# forcer les définitions sans clé à la fin (sqlite requière par exemple que
# primary key (columns) soit à la fin)
$tmp = cl::merge(...$definitions);
$definitions = [];
$constraints = [];
$index = 0;
foreach ($tmp as $col => $def) {
if ($col === $index) {
$index++;
$constraints[] = $def;
} else {
$definitions[$col] = $def;
}
}
return cl::merge($definitions, $constraints);
}
/** sérialiser les valeurs qui doivent l'être dans $values */
protected function serialize(CapacitorChannel $channel, ?array $values): ?array {
if ($values === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$row = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif ($channel->isSerialCol($key)) {
[$serialCol, $sumCol] = $channel->getSumCols($key);
if (array_key_exists($key, $values)) {
$sum = $channel->getSum($key, $values[$key]);
$row[$serialCol] = $sum[$serialCol];
if (array_key_exists($sumCol, $cols)) {
$row[$sumCol] = $sum[$sumCol];
}
}
} elseif (array_key_exists($key, $values)) {
$row[$col] = $values[$key];
}
}
return $row;
}
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$values = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif (!array_key_exists($col, $row)) {
} elseif ($channel->isSerialCol($key)) {
$value = $row[$col];
if ($value !== null) $value = $channel->unserialize($value);
$values[$key] = $value;
} else {
$values[$key] = $row[$col];
}
}
return $values;
}
function getPrimaryKeys(CapacitorChannel $channel): array {
$primaryKeys = $channel->getPrimaryKeys();
if ($primaryKeys === null) $primaryKeys = ["id_"];
return $primaryKeys;
}
function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array {
$primaryKeys = $this->getPrimaryKeys($channel);
$rowIds = cl::select($row, $primaryKeys);
if (cl::all_n($rowIds)) return null;
else return $rowIds;
}
protected function _createSql(CapacitorChannel $channel): array {
$cols = $this->ColumnDefinitions($channel);
return [
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $cols,
];
}
protected static function format_sql(CapacitorChannel $channel, string $sql): string {
$class = get_class($channel);
return <<<EOT
-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
-- autogénéré à partir de $class
$sql;
EOT;
}
abstract function _getCreateSql(CapacitorChannel $channel): string;
/** obtenir la requête SQL utilisée pour créer la table */
function getCreateSql(?string $channel): string {
return $this->_getCreateSql($this->getChannel($channel));
}
protected function _create(CapacitorChannel $channel): void {
$channel->ensureSetup();
if (!$channel->isCreated()) {
$this->db->exec($this->_createSql($channel));
$channel->setCreated();
}
}
/** tester si le canal spécifié existe */ /** tester si le canal spécifié existe */
abstract function _exists(CapacitorChannel $channel): bool;
function exists(?string $channel): bool { function exists(?string $channel): bool {
return $this->_exists($this->getChannel($channel)); return $this->_exists($this->getChannel($channel));
} }
abstract function _ensureExists(CapacitorChannel $channel): void;
/** s'assurer que le canal spécifié existe */ /** s'assurer que le canal spécifié existe */
function _ensureExists(CapacitorChannel $channel): void {
$this->_create($channel);
}
function ensureExists(?string $channel): void { function ensureExists(?string $channel): void {
$this->_ensureExists($this->getChannel($channel)); $this->_ensureExists($this->getChannel($channel));
} }
abstract function _reset(CapacitorChannel $channel): void;
/** supprimer le canal spécifié */ /** supprimer le canal spécifié */
function reset(?string $channel): void { function _reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->_reset($this->getChannel($channel)); $this->db->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
if ($recreate) $this->_ensureExists($channel);
} }
abstract function _charge(CapacitorChannel $channel, $item, ?callable $func, ?array $args): int; function reset(?string $channel, bool $recreate=false): void {
$this->_reset($this->getChannel($channel), $recreate);
}
/** /**
* charger une valeur dans le canal * charger une valeur dans le canal
* *
* Si $func!==null, après avoir calculé les valeurs des clés supplémentaires * Après avoir calculé les valeurs des clés supplémentaires
* avec {@link CapacitorChannel::getKeyValues()}, la fonction est appelée avec * avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions
* la signature ($item, $keyValues, $row, ...$args) * {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* Si la fonction retourne un tableau, il est utilisé pour modifier les valeurs * est appelée en fonction du type d'opération: création ou mise à jour
* insérées/mises à jour *
* Ensuite, si $func !== null, la fonction est appelée avec la signature de
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* en fonction du type d'opération: création ou mise à jour
*
* Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
* modifier les valeurs insérées/mises à jour. De plus, $values obtient la
* valeur finale des données insérées/mises à jour
*
* Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
* les méthodes {@link CapacitorChannel::getItemValues()},
* {@link CapacitorChannel::onCreate()} et/ou
* {@link CapacitorChannel::onUpdate()}
* *
* @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait * @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait
* déjà à l'identique dans le canal * déjà à l'identique dans le canal
*/ */
function charge(?string $channel, $item, ?callable $func=null, ?array $args=null): int { function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$values=null): int {
return $this->_charge($this->getChannel($channel), $item, $func, $args); $this->_create($channel);
$tableName = $channel->getTableName();
$db = $this->db();
$args ??= [];
$initFunc = [$channel, "getItemValues"];
$initArgs = $args;
func::ensure_func($initFunc, null, $initArgs);
$values = func::call($initFunc, $item, ...$initArgs);
if ($values === [false]) return 0;
$row = cl::merge(
$channel->getSum("item", $item),
$this->serialize($channel, $values));
$prow = null;
$rowIds = $this->getRowIds($channel, $row, $primaryKeys);
if ($rowIds !== null) {
# modification
$prow = $db->one([
"select",
"from" => $tableName,
"where" => $rowIds,
]);
} }
abstract function _discharge(CapacitorChannel $channel, bool $reset=true): iterable; $now = date("Y-m-d H:i:s");
$insert = null;
if ($prow === null) {
# création
$row = cl::merge($row, [
"created_" => $now,
"modified_" => $now,
]);
$insert = true;
$initFunc = [$channel, "onCreate"];
$initArgs = $args;
func::ensure_func($initFunc, null, $initArgs);
$values = $this->unserialize($channel, $row);
$pvalues = null;
} else {
# modification
# intégrer autant que possible les valeurs de prow dans row, de façon que
# l'utilisateur puisse voir clairement ce qui a été modifié
if ($channel->_wasSumModified("item", $row, $prow)) {
$insert = false;
$row = cl::merge($prow, $row, [
"modified_" => $now,
]);
} else {
$row = cl::merge($prow, $row);
}
$initFunc = [$channel, "onUpdate"];
$initArgs = $args;
func::ensure_func($initFunc, null, $initArgs);
$values = $this->unserialize($channel, $row);
$pvalues = $this->unserialize($channel, $prow);
}
$updates = func::call($initFunc, $item, $values, $pvalues, ...$initArgs);
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$values = cl::merge($values, $updates);
$row = cl::merge($row, $this->serialize($channel, $updates));
}
if ($func !== null) {
func::ensure_func($func, $channel, $args);
$updates = func::call($func, $item, $values, $pvalues, ...$args);
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$values = cl::merge($values, $updates);
$row = cl::merge($row, $this->serialize($channel, $updates));
}
}
# aucune modification
if ($insert === null) return 0;
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
}
$nbModified = 0;
try {
if ($insert) {
$id = $db->exec([
"insert",
"into" => $tableName,
"values" => $row,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$values[$primaryKeys[0]] = $id;
}
$nbModified = 1;
} else {
# calculer ce qui a changé pour ne mettre à jour que le nécessaire
$updates = [];
foreach ($row as $col => $value) {
if (array_key_exists($col, $rowIds)) {
# ne jamais mettre à jour la clé primaire
continue;
}
$pvalue = $prow[$col] ?? null;
if ($value !== ($pvalue)) {
$updates[$col] = $value;
}
}
if (count($updates) == 1 && array_key_first($updates) == "modified_") {
# si l'unique modification porte sur la date de modification, alors
# la ligne n'est pas modifiée. ce cas se présente quand on altère la
# valeur de $item
$updates = null;
}
if ($updates) {
$db->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => $rowIds,
]);
$nbModified = 1;
}
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $nbModified;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$values=null): int {
return $this->_charge($this->getChannel($channel), $item, $func, $args, $values);
}
/** décharger les données du canal spécifié */ /** décharger les données du canal spécifié */
function discharge(?string $channel, bool $reset=true): iterable { function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
$this->_create($channel);
$rows = $this->db()->all([
"select item__",
"from" => $channel->getTableName(),
]);
foreach ($rows as $row) {
yield unserialize($row['item__']);
}
if ($reset) $this->_reset($channel);
}
function discharge(?string $channel, bool $reset=true): Traversable {
return $this->_discharge($this->getChannel($channel), $reset); return $this->_discharge($this->getChannel($channel), $reset);
} }
abstract function _count(CapacitorChannel $channel, $filter): int; protected function _convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array {
$index = 0;
$fixed = [];
foreach ($filter as $key => $value) {
if ($key === $index) {
$index++;
if (is_array($value)) {
$value = $this->_convertValue2row($channel, $value, $cols);
}
$fixed[] = $value;
} else {
$col = "${key}__";
if (array_key_exists($col, $cols)) {
# colonne sérialisée
$fixed[$col] = $channel->serialize($value);
} else {
$fixed[$key] = $value;
}
}
}
return $fixed;
}
protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
if ($filter !== null && !is_array($filter)) {
$primaryKeys = $this->getPrimaryKeys($channel);
$id = $filter;
$channel->verifixId($id);
$filter = [$primaryKeys[0] => $id];
}
$cols = $this->ColumnDefinitions($channel);
if ($filter !== null) {
$filter = $this->_convertValue2row($channel, $filter, $cols);
}
}
/** indiquer le nombre d'éléments du canal spécifié */ /** indiquer le nombre d'éléments du canal spécifié */
function _count(CapacitorChannel $channel, $filter): int {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
return $this->db()->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
function count(?string $channel, $filter=null): int { function count(?string $channel, $filter=null): int {
return $this->_count($this->getChannel($channel), $filter); return $this->_count($this->getChannel($channel), $filter);
} }
abstract function _one(CapacitorChannel $channel, $filter): ?array;
/** /**
* obtenir la ligne correspondant au filtre sur le canal spécifié * obtenir la ligne correspondant au filtre sur le canal spécifié
* *
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter] * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/ */
function one(?string $channel, $filter): ?array { function _one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array {
return $this->_one($this->getChannel($channel), $filter); if ($filter === null) throw ValueException::null("filter");
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$row = $this->db()->one(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery));
return $this->unserialize($channel, $row);
} }
abstract function _all(CapacitorChannel $channel, $filter): iterable; function one(?string $channel, $filter, ?array $mergeQuery=null): ?array {
return $this->_one($this->getChannel($channel), $filter, $mergeQuery);
}
private function _allCached(string $id, CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$rows = $this->db()->all(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery), null, $this->getPrimaryKeys($channel));
if ($channel->isUseCache()) {
$cacheIds = [$id, get_class($channel)];
cache::get()->resetCached($cacheIds);
$rows = cache::new(null, $cacheIds, function() use ($rows) {
yield from $rows;
});
}
foreach ($rows as $key => $row) {
yield $key => $this->unserialize($channel, $row);
}
}
/** /**
* obtenir les lignes correspondant au filtre sur le canal spécifié * obtenir les lignes correspondant au filtre sur le canal spécifié
* *
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter] * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/ */
function all(?string $channel, $filter): iterable { function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
return $this->_one($this->getChannel($channel), $filter); return $this->_allCached("all", $channel, $filter, $mergeQuery);
} }
abstract function _each(CapacitorChannel $channel, $filter, ?callable $func, ?array $args): int; function all(?string $channel, $filter, $mergeQuery=null): Traversable {
return $this->_all($this->getChannel($channel), $filter, $mergeQuery);
}
/** /**
* appeler une fonction pour chaque élément du canal spécifié. * appeler une fonction pour chaque élément du canal spécifié.
* *
* $filter permet de filtrer parmi les élements chargés * $filter permet de filtrer parmi les élements chargés
* *
* $func est appelé avec la signature ($item, $row, ...$args). si la fonction * $func est appelé avec la signature de {@link CapacitorChannel::onEach()}
* retourne un tableau, il est utilisé pour mettre à jour la ligne * si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @param int $nbUpdated reçoit le nombre de lignes mises à jour
* @return int le nombre de lignes parcourues
*/
function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onEach;
func::ensure_func($func, $channel, $args);
$onEach = func::_prepare($func);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$nbUpdated = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$all = $this->_allCached("each", $channel, $filter, $mergeQuery);
foreach ($all as $values) {
$rowIds = $this->getRowIds($channel, $values);
$updates = func::_call($onEach, [$values["item"], $values, ...$args]);
if (is_array($updates) && $updates) {
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
}
$nbUpdated += $db->exec([
"update",
"table" => $tableName,
"values" => $this->serialize($channel, $updates),
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function each(?string $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
return $this->_each($this->getChannel($channel), $filter, $func, $args, $mergeQuery, $nbUpdated);
}
/**
* supprimer tous les éléments correspondant au filtre et pour lesquels la
* fonction retourne une valeur vraie si elle est spécifiée
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onDelete()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
* *
* @return int le nombre de lignes parcourues * @return int le nombre de lignes parcourues
*/ */
function each(?string $channel, $filter, ?callable $func=null, ?array $args=null): int { function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int {
return $this->_each($this->getChannel($channel), $filter, $func, $args); $this->_create($channel);
if ($func === null) $func = CapacitorChannel::onDelete;
func::ensure_func($func, $channel, $args);
$onEach = func::_prepare($func);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$all = $this->_allCached("delete", $channel, $filter);
foreach ($all as $values) {
$rowIds = $this->getRowIds($channel, $values);
$delete = boolval(func::_call($onEach, [$values["item"], $values, ...$args]));
if ($delete) {
$db->exec([
"delete",
"from" => $tableName,
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function delete(?string $channel, $filter, $func=null, ?array $args=null): int {
return $this->_delete($this->getChannel($channel), $filter, $func, $args);
} }
abstract function close(): void; abstract function close(): void;

19
php/src/db/IDatabase.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace nulib\db;
interface IDatabase extends ITransactor {
/**
* - si c'est un insert, retourner l'identifiant autogénéré de la ligne
* - sinon retourner le nombre de lignes modifiées en cas de succès, ou false
* en cas d'échec
*
* @return int|false
*/
function exec($query, ?array $params=null);
function get($query, ?array $params=null, bool $entireRow=false);
function one($query, ?array $params=null): ?array;
function all($query, ?array $params=null, $primaryKeys=null): iterable;
}

View File

@ -0,0 +1,30 @@
<?php
namespace nulib\db;
/**
* Class ITransactor: un objet qui peut faire des opérations dans une
* transaction
*/
interface ITransactor {
/**
* Indiquer qu'une transaction va être étendue à tous les objets mentionnés
*/
function willUpdate(...$transactors): self;
function inTransaction(): bool;
/**
* démarrer une transaction
*
* si $func!==null, l'apppeler. ensuite, si $commit===true, commiter la
* transaction. si une erreur se produit lors de l'appel de la fonction,
* annuler la transaction
*
* $func est appelée avec la signature ($this)
*/
function beginTransaction(?callable $func=null, bool $commit=true): void;
function commit(): void;
function rollback(): void;
}

7
php/src/db/TODO.md Normal file
View File

@ -0,0 +1,7 @@
# db/Capacitor
* charge() permet de spécifier la clé associée avec la valeur chargée, et
discharge() retourne les valeurs avec la clé primaire
* chargeAll() (ou peut-être chargeFrom()) permet de charger depuis un iterable
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

View File

@ -0,0 +1,39 @@
<?php
namespace nulib\db\_private;
trait Tcreate {
static function isa(string $sql): bool {
//return preg_match("/^create(?:\s+table)?\b/i", $sql);
#XXX implémentation minimale
return preg_match("/^create\s+table\b/i", $sql);
}
static function parse(array $query, ?array &$bindings=null): string {
#XXX implémentation minimale
$sql = [self::merge_seq($query)];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["table"];
## columns
$cols = $query["cols"];
$index = 0;
foreach ($cols as $col => &$definition) {
if ($col === $index) {
$index++;
} else {
$definition = "$col $definition";
}
}; unset($definition);
$sql[] = "(\n ".implode("\n, ", $cols)."\n)";
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace nulib\db\_private;
trait Tdelete {
static function isa(string $sql): bool {
return preg_match("/^delete(?:\s+from)?\b/i", $sql);
}
static function parse(array $query, ?array &$bindings=null): string {
#XXX implémentation minimale
$tmpsql = self::merge_seq($query);
self::consume('delete(?:\s+from)?\b', $tmpsql);
$sql = ["delete from", $tmpsql];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["from"];
## where
$where = $query["where"] ?? null;
if ($where !== null) {
self::parse_conds($where, $wheresql, $bindings);
if ($wheresql) {
$sql[] = "where";
$sql[] = implode(" and ", $wheresql);
}
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace nulib\db\_private;
use nulib\cl;
use nulib\ValueException;
trait Tgeneric {
static function isa(string $sql): bool {
return preg_match('/^(?:drop\s+table)\b/i', $sql);
}
static function parse(array $query, ?array &$bindings=null): string {
if (!cl::is_list($query)) {
throw new ValueException("Seuls les tableaux séquentiels sont supportés");
}
return self::merge_seq($query);
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace nulib\db\_private;
use nulib\cl;
use nulib\ValueException;
trait Tinsert {
static function isa(string $sql): bool {
return preg_match("/^insert\b/i", $sql);
}
/**
* parser une chaine de la forme
* "insert [into] [TABLE] [(COLS)] [values (VALUES)]"
*/
static function parse(array $query, ?array &$bindings=null): string {
# fusionner d'abord toutes les parties séquentielles
$usersql = $tmpsql = self::merge_seq($query);
### vérifier la présence des parties nécessaires
$sql = [];
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## insert
self::consume('insert\s*', $tmpsql);
$sql[] = "insert";
## into
self::consume('into\s*', $tmpsql);
$sql[] = "into";
$into = $query["into"] ?? null;
if (self::consume('([a-z_][a-z0-9_]*)\s*', $tmpsql, $ms)) {
if ($into === null) $into = $ms[1];
$sql[] = $into;
} elseif ($into !== null) {
$sql[] = $into;
} else {
throw new ValueException("expected table name: $usersql");
}
## cols & values
$usercols = [];
$uservalues = [];
if (self::consume('\(([^)]*)\)\s*', $tmpsql, $ms)) {
$usercols = array_merge($usercols, preg_split("/\s*,\s*/", $ms[1]));
}
$cols = cl::withn($query["cols"] ?? null);
$values = cl::withn($query["values"] ?? null);
$schema = $query["schema"] ?? null;
if ($cols === null) {
if ($usercols) {
$cols = $usercols;
} elseif ($values) {
$cols = array_keys($values);
$usercols = array_merge($usercols, $cols);
} elseif ($schema && is_array($schema)) {
#XXX implémenter support AssocSchema
$cols = array_keys($schema);
$usercols = array_merge($usercols, $cols);
}
}
if (self::consume('values\s+\(\s*(.*)\s*\)\s*', $tmpsql, $ms)) {
if ($ms[1]) $uservalues[] = $ms[1];
}
if ($cols !== null && !$uservalues) {
if (!$usercols) $usercols = $cols;
foreach ($cols as $col) {
$uservalues[] = ":$col";
$bindings[$col] = $values[$col] ?? null;
}
}
$sql[] = "(" . implode(", ", $usercols) . ")";
$sql[] = "values (" . implode(", ", $uservalues) . ")";
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
self::check_eof($tmpsql, $usersql);
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace nulib\db\_private;
use nulib\cl;
use nulib\str;
use nulib\ValueException;
trait Tselect {
static function isa(string $sql): bool {
return preg_match("/^select\b/i", $sql);
}
private static function add_prefix(string $col, ?string $prefix): string {
if ($prefix === null) return $col;
if (strpos($col, ".") !== false) return $col;
return "$prefix$col";
}
/**
* parser une chaine de la forme
* "select [COLS] [from TABLE] [where CONDS] [order by ORDERS] [group by GROUPS] [having CONDS]"
*/
static function parse(array $query, ?array &$bindings=null): string {
# fusionner d'abord toutes les parties séquentielles
$usersql = $tmpsql = self::merge_seq($query);
### vérifier la présence des parties nécessaires
$sql = [];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## select
self::consume('(select(?:\s*distinct)?)\s*', $tmpsql, $ms);
$sql[] = $ms[1];
## cols
$usercols = [];
if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) {
if ($ms[1]) $usercols[] = $ms[1];
}
$colPrefix = $query["col_prefix"] ?? null;
if ($colPrefix !== null) str::add_suffix($colPrefix, ".");
$tmpcols = cl::withn($query["cols"] ?? null);
$schema = $query["schema"] ?? null;
if ($tmpcols !== null) {
$cols = [];
$index = 0;
foreach ($tmpcols as $key => $col) {
if ($key === $index) {
$index++;
$cols[] = $col;
$usercols[] = self::add_prefix($col, $colPrefix);
} else {
$cols[] = $key;
$usercols[] = self::add_prefix($col, $colPrefix)." as $key";
}
}
} else {
$cols = null;
if ($schema && is_array($schema) && !in_array("*", $usercols)) {
$cols = array_keys($schema);
foreach ($cols as $col) {
$usercols[] = self::add_prefix($col, $colPrefix);
}
}
}
if (!$usercols && !$cols) $usercols = [self::add_prefix("*", $colPrefix)];
$sql[] = implode(", ", $usercols);
## from
$from = $query["from"] ?? null;
if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?=;?\s*$|\bwhere\b)', $tmpsql, $ms)) {
if ($from === null) $from = $ms[1];
$sql[] = "from";
$sql[] = $from;
} elseif ($from !== null) {
$sql[] = "from";
$sql[] = $from;
} else {
throw new ValueException("expected table name: $usersql");
}
## where
$userwhere = [];
if (self::consume('where\b\s*(.*?)(?=;?\s*$|\border\s+by\b)', $tmpsql, $ms)) {
if ($ms[1]) $userwhere[] = $ms[1];
}
$where = cl::withn($query["where"] ?? null);
if ($where !== null) self::parse_conds($where, $userwhere, $bindings);
if ($userwhere) {
$sql[] = "where";
$sql[] = implode(" and ", $userwhere);
}
## order by
$userorderby = [];
if (self::consume('order\s+by\b\s*(.*?)(?=;?\s*$|\bgroup\s+by\b)', $tmpsql, $ms)) {
if ($ms[1]) $userorderby[] = $ms[1];
}
$orderby = cl::withn($query["order by"] ?? null);
if ($orderby !== null) {
$index = 0;
foreach ($orderby as $key => $value) {
if ($key === $index) {
$userorderby[] = $value;
$index++;
} else {
if ($value === null) $value = false;
if (!is_bool($value)) {
$userorderby[] = "$key $value";
} elseif ($value) {
$userorderby[] = $key;
}
}
}
}
if ($userorderby) {
$sql[] = "order by";
$sql[] = implode(", ", $userorderby);
}
## group by
$usergroupby = [];
if (self::consume('group\s+by\b\s*(.*?)(?=;?\s*$|\bhaving\b)', $tmpsql, $ms)) {
if ($ms[1]) $usergroupby[] = $ms[1];
}
$groupby = cl::withn($query["group by"] ?? null);
if ($groupby !== null) {
$index = 0;
foreach ($groupby as $key => $value) {
if ($key === $index) {
$usergroupby[] = $value;
$index++;
} else {
if ($value === null) $value = false;
if (!is_bool($value)) {
$usergroupby[] = "$key $value";
} elseif ($value) {
$usergroupby[] = $key;
}
}
}
}
if ($usergroupby) {
$sql[] = "group by";
$sql[] = implode(", ", $usergroupby);
}
## having
$userhaving = [];
if (self::consume('having\b\s*(.*?)(?=;?\s*$)', $tmpsql, $ms)) {
if ($ms[1]) $userhaving[] = $ms[1];
}
$having = cl::withn($query["having"] ?? null);
if ($having !== null) self::parse_conds($having, $userhaving, $bindings);
if ($userhaving) {
$sql[] = "having";
$sql[] = implode(" and ", $userhaving);
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
self::check_eof($tmpsql, $usersql);
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace nulib\db\_private;
trait Tupdate {
static function isa(string $sql): bool {
return preg_match("/^update\b/i", $sql);
}
static function parse(array $query, ?array &$bindings=null): string {
#XXX implémentation minimale
$sql = [self::merge_seq($query)];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["table"];
## set
self::parse_set_values($query["values"], $setsql, $bindings);
$sql[] = "set";
$sql[] = implode(", ", $setsql);
## where
$where = $query["where"] ?? null;
if ($where !== null) {
self::parse_conds($where, $wheresql, $bindings);
if ($wheresql) {
$sql[] = "where";
$sql[] = implode(" and ", $wheresql);
}
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,262 @@
<?php
namespace nulib\db\_private;
use nulib\cl;
use nulib\str;
use nulib\ValueException;
abstract class _base {
protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool {
if (!preg_match("/^$pattern/i", $string, $ms)) return false;
$string = substr($string, strlen($ms[0]));
return true;
}
/** fusionner toutes les parties séquentielles d'une requête */
protected static function merge_seq(array $query): string {
$index = 0;
$sql = "";
foreach ($query as $key => $value) {
if ($key === $index) {
$index++;
if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) {
$sql .= " ";
}
$sql .= $value;
}
}
return $sql;
}
protected static function is_sep(&$cond): bool {
if (!is_string($cond)) return false;
if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false;
$cond = $ms[1];
return true;
}
static function parse_conds(?array $conds, ?array &$sql, ?array &$bindings): void {
if (!$conds) return;
$sep = null;
$index = 0;
$condsql = [];
foreach ($conds as $key => $cond) {
if ($key === $index) {
## séquentiel
if ($index === 0 && self::is_sep($cond)) {
$sep = $cond;
} elseif (is_bool($cond)) {
# ignorer les valeurs true et false
} elseif (is_array($cond)) {
# condition récursive
self::parse_conds($cond, $condsql, $bindings);
} else {
# condition litérale
$condsql[] = strval($cond);
}
$index++;
} elseif ($cond === false) {
## associatif
# condition litérale ignorée car condition false
} elseif ($cond === true) {
# condition litérale sélectionnée car condition true
$condsql[] = strval($key);
} else {
## associatif
# paramètre
$param0 = preg_replace('/^.+\./', "", $key);
$i = false;
if ($bindings !== null && array_key_exists($param0, $bindings)) {
$i = 2;
while (array_key_exists("$param0$i", $bindings)) {
$i++;
}
}
# value ou [operator, value]
$condprefix = $condsep = $condsuffix = null;
if (is_array($cond)) {
$condkey = 0;
$condkeys = array_keys($cond);
$op = null;
if (array_key_exists("op", $cond)) {
$op = $cond["op"];
} elseif (array_key_exists($condkey, $condkeys)) {
$op = $cond[$condkeys[$condkey]];
$condkey++;
}
$op = strtolower($op);
$condvalues = null;
switch ($op) {
case "between":
# ["between", $upper, $lower]
$condsep = " and ";
if (array_key_exists("lower", $cond)) {
$condvalues[] = $cond["lower"];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues[] = $cond[$condkeys[$condkey]];
$condkey++;
}
if (array_key_exists("upper", $cond)) {
$condvalues[] = $cond["upper"];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues[] = $cond[$condkeys[$condkey]];
$condkey++;
}
break;
case "in":
# ["in", $values]
$condprefix = "(";
$condsep = ", ";
$condsuffix = ")";
$condvalues = null;
if (array_key_exists("values", $cond)) {
$condvalues = cl::with($cond["values"]);
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues = cl::with($cond[$condkeys[$condkey]]);
$condkey++;
}
break;
case "null":
case "is null":
$op = "is null";
break;
case "not null":
case "is not null":
$op = "is not null";
break;
default:
if (array_key_exists("value", $cond)) {
$condvalues = [$cond["value"]];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues = [$cond[$condkeys[$condkey]]];
$condkey++;
}
}
} elseif ($cond !== null) {
$op = "=";
$condvalues = [$cond];
} else {
$op = "is null";
$condvalues = null;
}
$cond = [$key, $op];
if ($condvalues !== null) {
$parts = [];
foreach ($condvalues as $condvalue) {
if (is_array($condvalue)) {
$first = true;
foreach ($condvalue as $value) {
if ($first) {
$first = false;
} else {
if ($sep === null) $sep = "and";
$parts[] = " $sep ";
$parts[] = $key;
$parts[] = " $op ";
}
$param = "$param0$i";
$parts[] = ":$param";
$bindings[$param] = $value;
if ($i === false) $i = 2;
else $i++;
}
} else {
$param = "$param0$i";
$parts[] = ":$param";
$bindings[$param] = $condvalue;
if ($i === false) $i = 2;
else $i++;
}
}
$cond[] = $condprefix.implode($condsep, $parts).$condsuffix;
}
$condsql[] = implode(" ", $cond);
}
}
if ($sep === null) $sep = "and";
$count = count($condsql);
if ($count > 1) {
$sql[] = "(" . implode(" $sep ", $condsql) . ")";
} elseif ($count == 1) {
$sql[] = $condsql[0];
}
}
static function parse_set_values(?array $values, ?array &$sql, ?array &$bindings): void {
if (!$values) return;
$index = 0;
$parts = [];
foreach ($values as $key => $part) {
if ($key === $index) {
## séquentiel
if (is_array($part)) {
# paramètres récursifs
self::parse_set_values($part, $parts, $bindings);
} else {
# paramètre litéral
$parts[] = strval($part);
}
$index++;
} else {
## associatif
# paramètre
$param = $param0 = preg_replace('/^.+\./', "", $key);
if ($bindings !== null && array_key_exists($param0, $bindings)) {
$i = 2;
while (array_key_exists("$param0$i", $bindings)) {
$i++;
}
$param = "$param0$i";
}
# value
$value = $part;
$part = [$key, "="];
if ($value === null) {
$part[] = "null";
} else {
$part[] = ":$param";
$bindings[$param] = $value;
}
$parts[] = implode(" ", $part);
}
}
$sql = cl::merge($sql, $parts);
}
protected static function check_eof(string $tmpsql, string $usersql): void {
self::consume(';\s*', $tmpsql);
if ($tmpsql) {
throw new ValueException("unexpected value at end: $usersql");
}
}
abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void;
function __construct($sql, ?array $bindings=null) {
static::verifix($sql, $bindings, $meta);
$this->sql = $sql;
$this->bindings = $bindings;
$this->meta = $meta;
}
/** @var string */
protected $sql;
function getSql(): string {
return $this->sql;
}
/** @var ?array */
protected $bindings;
function getBindings(): ?array {
return $this->bindings;
}
/** @var ?array */
protected $meta;
function isInsert(): bool {
return ($this->meta["isa"] ?? null) === "insert";
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace nulib\db\_private;
class _create {
const SCHEMA = [
"prefix" => "?string",
"table" => "string",
"schema" => "?array",
"cols" => "?array",
"suffix" => "?string",
];
}

View File

@ -0,0 +1,11 @@
<?php
namespace nulib\db\_private;
class _delete {
const SCHEMA = [
"prefix" => "?string",
"from" => "?string",
"where" => "?array",
"suffix" => "?string",
];
}

View File

@ -0,0 +1,7 @@
<?php
namespace nulib\db\_private;
class _generic {
const SCHEMA = [
];
}

View File

@ -0,0 +1,13 @@
<?php
namespace nulib\db\_private;
class _insert {
const SCHEMA = [
"prefix" => "?string",
"into" => "?string",
"schema" => "?array",
"cols" => "?array",
"values" => "?array",
"suffix" => "?string",
];
}

View File

@ -0,0 +1,17 @@
<?php
namespace nulib\db\_private;
class _select {
const SCHEMA = [
"prefix" => "?string",
"schema" => "?array",
"cols" => "?array",
"col_prefix" => "?string",
"from" => "?string",
"where" => "?array",
"order by" => "?array",
"group by" => "?array",
"having" => "?array",
"suffix" => "?string",
];
}

View File

@ -0,0 +1,14 @@
<?php
namespace nulib\db\_private;
class _update {
const SCHEMA = [
"prefix" => "?string",
"table" => "?string",
"schema" => "?array",
"cols" => "?array",
"values" => "?array",
"where" => "?array",
"suffix" => "?string",
];
}

116
php/src/db/cache/CacheChannel.php vendored Normal file
View File

@ -0,0 +1,116 @@
<?php
namespace nulib\db\cache;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\php\time\DateTime;
use nulib\php\time\Delay;
class CacheChannel extends CapacitorChannel {
/** @var int durée de vie par défaut du cache */
const DURATION = "1D"; // jusqu'au lendemain
const INCLUDES = null;
const EXCLUDES = null;
const COLUMN_DEFINITIONS = [
"group_id" => "varchar(64) not null",
"id" => "varchar(64) not null",
"date_start" => "datetime",
"duration_" => "text",
"primary key (group_id, id)",
];
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];
}
function __construct(?string $duration=null, ?string $name=null) {
parent::__construct($name);
$this->duration = $duration ?? static::DURATION;
$this->includes = static::INCLUDES;
$this->excludes = static::EXCLUDES;
}
protected string $duration;
protected ?array $includes;
protected ?array $excludes;
function getItemValues($item): ?array {
return cl::merge(self::get_cache_ids($item), [
"item" => null,
]);
}
function onCreate($item, array $values, ?array $alwaysNull, ?string $duration=null): ?array {
$now = new DateTime();
$duration ??= $this->duration;
return [
"date_start" => $now,
"duration" => new Delay($duration, $now),
];
}
function onUpdate($item, array $values, array $pvalues, ?string $duration=null): ?array {
$now = new DateTime();
$duration ??= $this->duration;
return [
"date_start" => $now,
"duration" => new Delay($duration, $now),
];
}
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;
}
$found = false;
$expired = false;
$this->each($cacheIds,
function($item, $values) use (&$found, &$expired) {
$found = true;
$expired = $values["duration"]->isElapsed();
});
return !$found || $expired;
}
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);
}
}

51
php/src/db/cache/RowsChannel.php vendored Normal file
View File

@ -0,0 +1,51 @@
<?php
namespace nulib\db\cache;
use Closure;
use IteratorAggregate;
use nulib\cl;
use nulib\db\CapacitorChannel;
use Traversable;
class RowsChannel 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);
}
}

37
php/src/db/cache/cache.php vendored Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace nulib\db\cache;
use nulib\db\Capacitor;
use nulib\db\CapacitorStorage;
use nulib\db\sqlite\SqliteStorage;
class cache {
protected static ?CapacitorStorage $storage = null;
static function set_storage(CapacitorStorage $storage): CapacitorStorage {
return self::$storage = $storage;
}
protected static function get_storage(): CapacitorStorage {
return self::$storage ??= new SqliteStorage("");
}
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 get(): CacheChannel {
if (self::$channel !== null) return self::$channel;
else return self::set(null);
}
static function new(?RowsChannel $channel, $id=null, ?callable $builder=null): RowsChannel {
$channel ??= new RowsChannel($id, $builder);
new Capacitor(self::get_storage(), $channel);
return $channel;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace nulib\db\mysql;
use nulib\db\pdo\Pdo;
class Mysql extends Pdo {
function getDbname(): ?string {
$url = $this->dbconn["name"] ?? null;
if ($url !== null && preg_match('/^mysql(?::|.*;)dbname=([^;]+)/i', $url, $ms)) {
return $ms[1];
}
return null;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace nulib\db\mysql;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
/**
* Class MysqlStorage
*/
class MysqlStorage extends CapacitorStorage {
function __construct($mysql) {
$this->db = Mysql::with($mysql);
}
/** @var Mysql */
protected $db;
function db(): Mysql {
return $this->db;
}
const PRIMARY_KEY_DEFINITION = [
"id_" => "integer primary key auto_increment",
];
function _getCreateSql(CapacitorChannel $channel): string {
$query = new _query_base($this->_createSql($channel));
return self::format_sql($channel, $query->getSql());
}
function _exists(CapacitorChannel $channel): bool {
$db = $this->db;
$tableName = $db->get([
"select table_name from information_schema.tables",
"where" => [
"table_schema" => $db->getDbname(),
"table_name" => $channel->getTableName(),
],
]);
return $tableName !== null;
}
function close(): void {
$this->db->close();
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace nulib\db\mysql;
use nulib\ValueException;
class _query_base extends \nulib\db\pdo\_query_base {
protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_query_create::isa($prefix)) {
$sql = _query_create::parse($sql, $bindinds);
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_query_select::isa($prefix)) {
$sql = _query_select::parse($sql, $bindinds);
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_query_insert::isa($prefix)) {
$sql = _query_insert::parse($sql, $bindinds);
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_query_update::isa($prefix)) {
$sql = _query_update::parse($sql, $bindinds);
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_query_delete::isa($prefix)) {
$sql = _query_delete::parse($sql, $bindinds);
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_query_generic::isa($prefix)) {
$sql = _query_generic::parse($sql, $bindinds);
$meta = ["isa" => "generic", "type" => null];
} else {
throw ValueException::invalid_kind($sql, "query");
}
} else {
if (!is_string($sql)) $sql = strval($sql);
if (_query_create::isa($sql)) {
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_query_select::isa($sql)) {
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_query_insert::isa($sql)) {
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_query_update::isa($sql)) {
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_query_delete::isa($sql)) {
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_query_generic::isa($sql)) {
$meta = ["isa" => "generic", "type" => null];
} else {
$meta = ["isa" => "generic", "type" => null];
}
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_create;
use nulib\db\_private\Tcreate;
class _query_create extends _query_base {
use Tcreate;
const SCHEMA = _create::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_delete;
use nulib\db\_private\Tdelete;
class _query_delete extends _query_base {
use Tdelete;
const SCHEMA = _delete::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_generic;
use nulib\db\_private\Tgeneric;
class _query_generic extends _query_base {
use Tgeneric;
const SCHEMA = _generic::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_insert;
use nulib\db\_private\Tinsert;
class _query_insert extends _query_base {
use Tinsert;
const SCHEMA = _insert::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_select;
use nulib\db\_private\Tselect;
class _query_select extends _query_base {
use Tselect;
const SCHEMA = _select::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_update;
use nulib\db\_private\Tupdate;
class _query_update extends _query_base {
use Tupdate;
const SCHEMA = _update::SCHEMA;
}

View File

@ -0,0 +1,12 @@
<?php
namespace nulib\db\mysql;
/**
* Class query: classe outil temporaire pour générer les requêtes
*/
class query extends _query_base {
static function with($sql, ?array $params=null): array {
self::verifix($sql, $params);
return [$sql, $params];
}
}

304
php/src/db/pdo/Pdo.php Normal file
View File

@ -0,0 +1,304 @@
<?php
namespace nulib\db\pdo;
use Generator;
use nulib\cl;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\php\func;
use nulib\php\time\Date;
use nulib\php\time\DateTime;
use nulib\ValueException;
class Pdo implements IDatabase {
static function with($pdo, ?array $params=null): self {
if ($pdo instanceof static) {
return $pdo;
} elseif ($pdo instanceof self) {
# recréer avec les mêmes paramètres
return new static(null, cl::merge([
"dbconn" => $pdo->dbconn,
"options" => $pdo->options,
"config" => $pdo->config,
"migrate" => $pdo->migration,
], $params));
} else {
return new static($pdo, $params);
}
}
static function config_errmodeException_lowerCase(self $pdo): void {
$pdo->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->db->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER);
}
const CONFIG_errmodeException_lowerCase = [self::class, "config_errmodeException_lowerCase"];
static function config_unbufferedQueries(self $pdo): void {
$pdo->db->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
}
const CONFIG_unbufferedQueries = [self::class, "config_unbufferedQueries"];
protected const OPTIONS = [
\PDO::ATTR_PERSISTENT => true,
];
protected const DEFAULT_CONFIG = [
self::CONFIG_errmodeException_lowerCase,
];
protected const CONFIG = null;
protected const MIGRATE = null;
const dbconn_SCHEMA = [
"name" => "string",
"user" => "?string",
"pass" => "?string",
];
const params_SCHEMA = [
"dbconn" => ["array"],
"options" => ["?array|callable"],
"replace_config" => ["?array|callable"],
"config" => ["?array|callable"],
"migrate" => ["?array|string|callable"],
"auto_open" => ["bool", true],
];
function __construct($dbconn=null, ?array $params=null) {
if ($dbconn !== null) {
if (!is_array($dbconn)) {
$dbconn = ["name" => $dbconn];
#XXX à terme, il faudra interroger config
#$tmp = config::db($dbconn);
#if ($tmp !== null) $dbconn = $tmp;
#else $dbconn = ["name" => $dbconn];
}
$params["dbconn"] = $dbconn;
}
# dbconn
$this->dbconn = $params["dbconn"] ?? null;
$this->dbconn["name"] ??= null;
$this->dbconn["user"] ??= null;
$this->dbconn["pass"] ??= null;
# options
$this->options = $params["options"] ?? static::OPTIONS;
# configuration
$config = $params["replace_config"] ?? null;
if ($config === null) {
$config = $params["config"] ?? static::CONFIG;
if (is_callable($config)) $config = [$config];
$config = cl::merge(static::DEFAULT_CONFIG, $config);
}
$this->config = $config;
# migrations
$this->migration = $params["migrate"] ?? static::MIGRATE;
#
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open();
}
}
protected ?array $dbconn;
/** @var array|callable */
protected array $options;
/** @var array|string|callable */
protected $config;
/** @var array|string|callable */
protected $migration;
protected ?\PDO $db = null;
function open(): self {
if ($this->db === null) {
$dbconn = $this->dbconn;
$options = $this->options;
if (is_callable($options)) {
func::ensure_func($options, $this, $args);
$options = func::call($options, ...$args);
}
$this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options);
_config::with($this->config)->configure($this);
//_migration::with($this->migration)->migrate($this);
}
return $this;
}
function close(): void {
$this->db = null;
}
protected function db(): \PDO {
$this->open();
return $this->db;
}
/** @return int|false */
function _exec(string $query) {
return $this->db()->exec($query);
}
private static function is_insert(?string $sql): bool {
if ($sql === null) return false;
return preg_match('/^\s*insert\b/i', $sql);
}
function exec($query, ?array $params=null) {
$db = $this->db();
$query = new _query_base($query, $params);
if ($query->useStmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return false;
if ($query->isInsert()) return $db->lastInsertId();
else return $stmt->rowCount();
} else {
$rowCount = $db->exec($sql);
if (self::is_insert($sql)) return $db->lastInsertId();
else return $rowCount;
}
}
/** @var ITransactor[] */
protected ?array $transactors = null;
function willUpdate(...$transactors): self {
foreach ($transactors as $transactor) {
if ($transactor instanceof ITransactor) {
$this->transactors[] = $transactor;
$transactor->willUpdate();
} else {
throw ValueException::invalid_type($transactor, ITransactor::class);
}
}
return $this;
}
function inTransaction(): bool {
return $this->db()->inTransaction();
}
function beginTransaction(?callable $func=null, bool $commit=true): void {
$this->db()->beginTransaction();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->beginTransaction();
}
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
}
} finally {
if ($commit && !$commited) $this->rollback();
}
}
}
function commit(): void {
$this->db()->commit();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->commit();
}
}
}
function rollback(): void {
$this->db()->rollBack();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->rollback();
}
}
}
/**
* Tester si $date est une date/heure valide de la forme "YYYY-mm-dd HH:MM:SS"
*
* Si oui, $ms obtient les 6 éléments de la chaine
*/
static function is_datetime($date, ?array &$ms=null): bool {
return is_string($date) && preg_match('/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $date, $ms);
}
/**
* Tester si $date est une date valide de la forme "YYYY-mm-dd [00:00:00]"
*
* Si oui, $ms obtient les 3 éléments de la chaine
*/
static function is_date($date, ?array &$ms=null): bool {
return is_string($date) && preg_match('/^(\d{4})-(\d{2})-(\d{2})(?: 00:00:00)?$/', $date, $ms);
}
function verifixRow(array &$row) {
foreach ($row as &$value) {
if (self::is_date($value)) {
$value = new Date($value);
} elseif (self::is_datetime($value)) {
$value = new DateTime($value);
}
}; unset($value);
}
function get($query, ?array $params=null, bool $entireRow=false) {
$db = $this->db();
$query = new _query_base($query, $params);
$stmt = null;
try {
/** @var \PDOStatement $stmt */
if ($query->useStmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return null;
} else {
$stmt = $db->query($sql);
}
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($row === false) return null;
$this->verifixRow($row);
if ($entireRow) return $row;
else return cl::first($row);
} finally {
if ($stmt instanceof \PDOStatement) $stmt->closeCursor();
}
}
function one($query, ?array $params=null): ?array {
return $this->get($query, $params, true);
}
/**
* si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s)
* spécifiée(s)
*/
function all($query, ?array $params=null, $primaryKeys=null): Generator {
$db = $this->db();
$query = new _query_base($query, $params);
$stmt = null;
try {
/** @var \PDOStatement $stmt */
if ($query->useStmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return;
} else {
$stmt = $db->query($sql);
}
if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys);
while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
$this->verifixRow($row);
if ($primaryKeys !== null) {
$key = implode("-", cl::select($row, $primaryKeys));
yield $key => $row;
} else {
yield $row;
}
}
} finally {
if ($stmt instanceof \PDOStatement) $stmt->closeCursor();
}
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace nulib\db\pdo;
use nulib\php\func;
class _config {
static function with($configs): self {
if ($configs instanceof static) return $configs;
return new static($configs);
}
const CONFIG = null;
function __construct($configs) {
if ($configs === null) $configs = static::CONFIG;
if ($configs === null) $configs = [];
elseif (is_string($configs)) $configs = [$configs];
elseif (is_callable($configs)) $configs = [$configs];
elseif (!is_array($configs)) $configs = [strval($configs)];
$this->configs = $configs;
}
/** @var array */
protected $configs;
function configure(Pdo $pdo): void {
foreach ($this->configs as $key => $config) {
if (is_string($config) && !func::is_method($config)) {
$pdo->exec($config);
} else {
func::ensure_func($config, $this, $args);
func::call($config, $pdo, $key, ...$args);
}
}
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace nulib\db\pdo;
use DateTimeInterface;
use nulib\db\_private\_base;
use nulib\php\time\Date;
use nulib\php\time\DateTime;
use nulib\str;
use nulib\ValueException;
class _query_base extends _base {
protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_query_create::isa($prefix)) {
$sql = _query_create::parse($sql, $bindinds);
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_query_select::isa($prefix)) {
$sql = _query_select::parse($sql, $bindinds);
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_query_insert::isa($prefix)) {
$sql = _query_insert::parse($sql, $bindinds);
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_query_update::isa($prefix)) {
$sql = _query_update::parse($sql, $bindinds);
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_query_delete::isa($prefix)) {
$sql = _query_delete::parse($sql, $bindinds);
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_query_generic::isa($prefix)) {
$sql = _query_generic::parse($sql, $bindinds);
$meta = ["isa" => "generic", "type" => null];
} else {
throw ValueException::invalid_kind($sql, "query");
}
} else {
if (!is_string($sql)) $sql = strval($sql);
if (_query_create::isa($sql)) {
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_query_select::isa($sql)) {
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_query_insert::isa($sql)) {
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_query_update::isa($sql)) {
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_query_delete::isa($sql)) {
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_query_generic::isa($sql)) {
$meta = ["isa" => "generic", "type" => null];
} else {
$meta = ["isa" => "generic", "type" => null];
}
}
}
static function is_sqldate(string $date): bool {
return preg_match('/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2})?$/', $date);
}
protected function verifixBindings(&$value): void {
if ($value instanceof Date) {
$value = $value->format('Y-m-d');
} elseif ($value instanceof DateTime) {
$value = $value->format('Y-m-d H:i:s');
} elseif ($value instanceof DateTimeInterface) {
$value = $value->format('Y-m-d H:i:s');
str::del_suffix($value, " 00:00:00");
} elseif (is_string($value)) {
if (self::is_sqldate($value)) {
# déjà dans le bon format
} elseif (Date::isa_date($value, true)) {
$value = new Date($value);
$value = $value->format('Y-m-d');
} elseif (DateTime::isa_datetime($value, true)) {
$value = new DateTime($value);
$value = $value->format('Y-m-d H:i:s');
}
} elseif (is_bool($value)) {
$value = $value? 1: 0;
}
}
const DEBUG_QUERIES = false;
function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) { #XXX
error_log($this->sql);
//error_log(var_export($this->bindings, true));
}
if ($this->bindings !== null) {
$stmt = $db->prepare($this->sql);
foreach ($this->bindings as $name => $value) {
$this->verifixBindings($value);
$stmt->bindValue($name, $value);
}
return true;
} else {
$sql = $this->sql;
return false;
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_create;
use nulib\db\_private\Tcreate;
class _query_create extends _query_base {
use Tcreate;
const SCHEMA = _create::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_delete;
use nulib\db\_private\Tdelete;
class _query_delete extends _query_base {
use Tdelete;
const SCHEMA = _delete::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_generic;
use nulib\db\_private\Tgeneric;
class _query_generic extends _query_base {
use Tgeneric;
const SCHEMA = _generic::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_insert;
use nulib\db\_private\Tinsert;
class _query_insert extends _query_base {
use Tinsert;
const SCHEMA = _insert::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_select;
use nulib\db\_private\Tselect;
class _query_select extends _query_base {
use Tselect;
const SCHEMA = _select::SCHEMA;
}

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_update;
use nulib\db\_private\Tupdate;
class _query_update extends _query_base {
use Tupdate;
const SCHEMA = _update::SCHEMA;
}

View File

@ -3,6 +3,10 @@ namespace nulib\db\sqlite;
use Generator; use Generator;
use nulib\cl; use nulib\cl;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\php\func;
use nulib\ValueException;
use SQLite3; use SQLite3;
use SQLite3Result; use SQLite3Result;
use SQLite3Stmt; use SQLite3Stmt;
@ -10,7 +14,7 @@ use SQLite3Stmt;
/** /**
* Class Sqlite: frontend vers une base de données sqlite3 * Class Sqlite: frontend vers une base de données sqlite3
*/ */
class Sqlite { class Sqlite implements IDatabase {
static function with($sqlite, ?array $params=null): self { static function with($sqlite, ?array $params=null): self {
if ($sqlite instanceof static) { if ($sqlite instanceof static) {
return $sqlite; return $sqlite;
@ -34,27 +38,44 @@ class Sqlite {
static function config_enableExceptions(self $sqlite): void { static function config_enableExceptions(self $sqlite): void {
$sqlite->db->enableExceptions(true); $sqlite->db->enableExceptions(true);
} }
const CONFIG_enableExceptions = [self::class, "config_enableExceptions"];
/**
* @var int temps maximum à attendre que la base soit accessible si elle est
* verrouillée
*/
protected const BUSY_TIMEOUT = 30 * 1000;
static function config_busyTimeout(self $sqlite): void {
$sqlite->db->busyTimeout(static::BUSY_TIMEOUT);
}
const CONFIG_busyTimeout = [self::class, "config_busyTimeout"];
static function config_enableWalIfAllowed(self $sqlite): void { static function config_enableWalIfAllowed(self $sqlite): void {
if ($sqlite->isWalAllowed()) { if ($sqlite->isWalAllowed()) {
$sqlite->db->exec("PRAGMA journal_mode=WAL"); $sqlite->db->exec("PRAGMA journal_mode=WAL");
} }
} }
const CONFIG_enableWalIfAllowed = [self::class, "config_enableWalIfAllowed"];
const ALLOW_WAL = null; const ALLOW_WAL = null;
const CONFIG = [ const DEFAULT_CONFIG = [
[self::class, "config_enableExceptions"], self::CONFIG_enableExceptions,
[self::class, "config_enableWalIfAllowed"], self::CONFIG_busyTimeout,
self::CONFIG_enableWalIfAllowed,
]; ];
const CONFIG = null;
const MIGRATE = null; const MIGRATE = null;
const SCHEMA = [ const params_SCHEMA = [
"file" => ["string", ""], "file" => ["string", ""],
"flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE], "flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE],
"encryption_key" => ["string", ""], "encryption_key" => ["string", ""],
"allow_wal" => ["?bool"], "allow_wal" => ["?bool"],
"replace_config" => ["?array|callable"],
"config" => ["?array|callable"], "config" => ["?array|callable"],
"migrate" => ["?array|string|callable"], "migrate" => ["?array|string|callable"],
"auto_open" => ["bool", true], "auto_open" => ["bool", true],
@ -63,24 +84,31 @@ class Sqlite {
function __construct(?string $file=null, ?array $params=null) { function __construct(?string $file=null, ?array $params=null) {
if ($file !== null) $params["file"] = $file; if ($file !== null) $params["file"] = $file;
##schéma ##schéma
$defaultFile = self::SCHEMA["file"][1]; $defaultFile = self::params_SCHEMA["file"][1];
$this->file = $file = strval($params["file"] ?? $defaultFile); $this->file = $file = strval($params["file"] ?? $defaultFile);
$inMemory = $file === ":memory:"; $inMemory = $file === ":memory:" || $file === "";
# #
$defaultFlags = self::SCHEMA["flags"][1]; $defaultFlags = self::params_SCHEMA["flags"][1];
$this->flags = intval($params["flags"] ?? $defaultFlags); $this->flags = intval($params["flags"] ?? $defaultFlags);
# #
$defaultEncryptionKey = self::SCHEMA["encryption_key"][1]; $defaultEncryptionKey = self::params_SCHEMA["encryption_key"][1];
$this->encryptionKey = strval($params["encryption_key"] ?? $defaultEncryptionKey); $this->encryptionKey = strval($params["encryption_key"] ?? $defaultEncryptionKey);
# #
$defaultAllowWal = static::ALLOW_WAL ?? !$inMemory; $defaultAllowWal = static::ALLOW_WAL ?? !$inMemory;
$this->allowWal = $params["allow_wal"] ?? $defaultAllowWal; $this->allowWal = $params["allow_wal"] ?? $defaultAllowWal;
# configuration # configuration
$this->config = $params["config"] ?? static::CONFIG; $config = $params["replace_config"] ?? null;
if ($config === null) {
$config = $params["config"] ?? static::CONFIG;
if (is_callable($config)) $config = [$config];
$config = cl::merge(static::DEFAULT_CONFIG, $config);
}
$this->config = $config;
# migrations # migrations
$this->migration = $params["migrate"] ?? static::MIGRATE; $this->migration = $params["migrate"] ?? static::MIGRATE;
# #
$defaultAutoOpen = self::SCHEMA["auto_open"][1]; $defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
$this->inTransaction = false;
if ($params["auto_open"] ?? $defaultAutoOpen) { if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open(); $this->open();
} }
@ -112,11 +140,14 @@ class Sqlite {
/** @var SQLite3 */ /** @var SQLite3 */
protected $db; protected $db;
protected bool $inTransaction;
function open(): self { function open(): self {
if ($this->db === null) { if ($this->db === null) {
$this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
_config::with($this->config)->configure($this); _config::with($this->config)->configure($this);
_migration::with($this->migration)->migrate($this); _migration::with($this->migration)->migrate($this);
$this->inTransaction = false;
} }
return $this; return $this;
} }
@ -125,6 +156,7 @@ class Sqlite {
if ($this->db !== null) { if ($this->db !== null) {
$this->db->close(); $this->db->close();
$this->db = null; $this->db = null;
$this->inTransaction = false;
} }
} }
@ -145,30 +177,92 @@ class Sqlite {
return $this->db()->exec($query); return $this->db()->exec($query);
} }
function exec($query, ?array $params=null): bool { private static function is_insert(?string $sql): bool {
if ($sql === null) return false;
return preg_match('/^\s*insert\b/i', $sql);
}
function exec($query, ?array $params=null) {
$db = $this->db(); $db = $this->db();
$query = new _query($query, $params); $query = new _query_base($query, $params);
if ($query->useStmt($db, $stmt, $sql)) { if ($query->useStmt($db, $stmt, $sql)) {
try { try {
return $stmt->execute()->finalize(); $result = $stmt->execute();
if ($result === false) return false;
$result->finalize();
if ($query->isInsert()) return $db->lastInsertRowID();
else return $db->changes();
} finally { } finally {
$stmt->close(); $stmt->close();
} }
} else { } else {
return $db->exec($sql); $result = $db->exec($sql);
if ($result === false) return false;
if (self::is_insert($sql)) return $db->lastInsertRowID();
else return $db->changes();
} }
} }
function beginTransaction(): void { /** @var ITransactor[] */
protected ?array $transactors = null;
function willUpdate(...$transactors): self {
foreach ($transactors as $transactor) {
if ($transactor instanceof ITransactor) {
$this->transactors[] = $transactor;
$transactor->willUpdate();
} else {
throw ValueException::invalid_type($transactor, ITransactor::class);
}
}
return $this;
}
function inTransaction(): bool {
#XXX très imparfait, mais y'a rien de mieux pour le moment :-(
return $this->inTransaction;
}
function beginTransaction(?callable $func=null, bool $commit=true): void {
$this->db()->exec("begin"); $this->db()->exec("begin");
$this->inTransaction = true;
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->beginTransaction();
}
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
}
} finally {
if ($commit && !$commited) $this->rollback();
}
}
} }
function commit(): void { function commit(): void {
$this->inTransaction = false;
$this->db()->exec("commit"); $this->db()->exec("commit");
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->commit();
}
}
} }
function rollback(): void { function rollback(): void {
$this->db()->exec("commit"); $this->inTransaction = false;
$this->db()->exec("rollback");
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->rollback();
}
}
} }
function _get(string $query, bool $entireRow=false) { function _get(string $query, bool $entireRow=false) {
@ -177,7 +271,7 @@ class Sqlite {
function get($query, ?array $params=null, bool $entireRow=false) { function get($query, ?array $params=null, bool $entireRow=false) {
$db = $this->db(); $db = $this->db();
$query = new _query($query, $params); $query = new _query_base($query, $params);
if ($query->useStmt($db, $stmt, $sql)) { if ($query->useStmt($db, $stmt, $sql)) {
try { try {
$result = $this->checkResult($stmt->execute()); $result = $this->checkResult($stmt->execute());
@ -201,26 +295,36 @@ class Sqlite {
return $this->get($query, $params, true); return $this->get($query, $params, true);
} }
protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null): Generator { protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator {
if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys);
try { try {
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) { while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
if ($primaryKeys !== null) {
$key = implode("-", cl::select($row, $primaryKeys));
yield $key => $row;
} else {
yield $row; yield $row;
} }
}
} finally { } finally {
$result->finalize(); $result->finalize();
if ($stmt !== null) $stmt->close(); if ($stmt !== null) $stmt->close();
} }
} }
function all($query, ?array $params=null): iterable { /**
* si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s)
* spécifiée(s)
*/
function all($query, ?array $params=null, $primaryKeys=null): iterable {
$db = $this->db(); $db = $this->db();
$query = new _query($query, $params); $query = new _query_base($query, $params);
if ($query->useStmt($db, $stmt, $sql)) { if ($query->useStmt($db, $stmt, $sql)) {
$result = $this->checkResult($stmt->execute()); $result = $this->checkResult($stmt->execute());
return $this->_fetchResult($result, $stmt); return $this->_fetchResult($result, $stmt, $primaryKeys);
} else { } else {
$result = $this->checkResult($db->query($sql)); $result = $this->checkResult($db->query($sql));
return $this->_fetchResult($result); return $this->_fetchResult($result, null, $primaryKeys);
} }
} }
} }

View File

@ -1,116 +1,35 @@
<?php <?php
namespace nulib\db\sqlite; namespace nulib\db\sqlite;
use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage; use nulib\db\CapacitorStorage;
use nulib\php\func;
use nulib\str;
use nulib\ValueException;
/** /**
* Class SqliteStorage * Class SqliteStorage
*/ */
class SqliteStorage extends CapacitorStorage { class SqliteStorage extends CapacitorStorage {
function __construct($sqlite) { function __construct($sqlite) {
$this->sqlite = Sqlite::with($sqlite); $this->db = Sqlite::with($sqlite);
} }
/** @var Sqlite */ /** @var Sqlite */
protected $sqlite; protected $db;
function sqlite(): Sqlite { function db(): Sqlite {
return $this->sqlite; return $this->db;
} }
const KEY_DEFINITIONS = [ const PRIMARY_KEY_DEFINITION = [
"id_" => "integer primary key autoincrement", "id_" => "integer primary key autoincrement",
"item__" => "text",
"sum_" => "varchar(40)",
"created_" => "datetime",
"modified_" => "datetime",
]; ];
/** sérialiser les valeurs qui doivent l'être dans $values */ function _getCreateSql(CapacitorChannel $channel): string {
protected function serialize(CapacitorChannel $channel, ?array $values): ?array { $query = new _query_base($this->_createSql($channel));
if ($values === null) return null; return self::format_sql($channel, $query->getSql());
$columns = cl::merge(self::KEY_DEFINITIONS, $channel->getKeyDefinitions());
$index = 0;
$row = [];
foreach (array_keys($columns) as $column) {
$key = $column;
if ($key === $index) {
$index++;
continue;
} elseif (str::del_suffix($key, "__")) {
if (!array_key_exists($key, $values)) continue;
$value = $values[$key];
if ($value !== null) $value = serialize($value);
} else {
if (!array_key_exists($key, $values)) continue;
$value = $values[$key];
}
$row[$column] = $value;
}
return $row;
}
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$columns = cl::merge(self::KEY_DEFINITIONS, $channel->getKeyDefinitions());
$index = 0;
$values = [];
foreach (array_keys($columns) as $column) {
$key = $column;
if ($key === $index) {
$index++;
continue;
} elseif (!array_key_exists($column, $row)) {
continue;
} elseif (str::del_suffix($key, "__")) {
$value = $row[$column];
if ($value !== null) $value = unserialize($value);
} else {
$value = $row[$column];
}
$values[$key] = $value;
}
return $values;
}
protected function _create(CapacitorChannel $channel): void {
if (!$channel->isCreated()) {
$columns = cl::merge(self::KEY_DEFINITIONS, $channel->getKeyDefinitions());
$this->sqlite->exec([
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $columns,
]);
$channel->setCreated();
}
}
/** @var CapacitorChannel[] */
protected $channels;
function addChannel(CapacitorChannel $channel): CapacitorChannel {
$this->_create($channel);
$this->channels[$channel->getName()] = $channel;
return $channel;
}
protected function getChannel(?string $name): CapacitorChannel {
$name = CapacitorChannel::verifix_name($name);
$channel = $this->channels[$name] ?? null;
if ($channel === null) {
$channel = $this->addChannel(new CapacitorChannel($name));
}
return $channel;
} }
function _exists(CapacitorChannel $channel): bool { function _exists(CapacitorChannel $channel): bool {
$tableName = $this->sqlite->get([ $tableName = $this->db->get([
"select name from sqlite_schema", "select name from sqlite_schema",
"where" => [ "where" => [
"name" => $channel->getTableName(), "name" => $channel->getTableName(),
@ -119,191 +38,7 @@ class SqliteStorage extends CapacitorStorage {
return $tableName !== null; return $tableName !== null;
} }
function _ensureExists(CapacitorChannel $channel): void {
$this->_create($channel);
}
function _reset(CapacitorChannel $channel): void {
$this->sqlite->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
}
function _charge(CapacitorChannel $channel, $item, ?callable $func, ?array $args): int {
$this->_create($channel);
$now = date("Y-m-d H:i:s");
$item__ = serialize($item);
$sum_ = sha1($item__);
$row = cl::merge([
"item__" => $item__,
"sum_" => $sum_,
], $this->unserialize($channel, $channel->getKeyValues($item)));
$prow = null;
$id_ = $row["id_"] ?? null;
if ($id_ !== null) {
# modification
$prow = $this->sqlite->one([
"select id_, item__, sum_, created_, modified_",
"from" => $channel->getTableName(),
"where" => ["id_" => $id_],
]);
}
$insert = null;
if ($prow === null) {
# création
$row = cl::merge($row, [
"created_" => $now,
"modified_" => $now,
]);
$insert = true;
} elseif ($sum_ !== $prow["sum_"]) {
# modification
$row = cl::merge($row, [
"modified_" => $now,
]);
$insert = false;
}
if ($func === null) $func = [$channel, "onCharge"];
$onCharge = func::_prepare($func);
$args ??= [];
$values = $this->unserialize($channel, $row);
$pvalues = $this->unserialize($channel, $prow);
$updates = func::_call($onCharge, [$item, $values, $pvalues, ...$args]);
if (is_array($updates)) {
$updates = $this->serialize($channel, $updates);
if (array_key_exists("item__", $updates)) {
# si item a été mis à jour, il faut mettre à jour sum_
$updates["sum_"] = sha1($updates["item__"]);
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
}
$row = cl::merge($row, $updates);
}
if ($insert === null) {
# aucune modification
return 0;
} elseif ($insert) {
$this->sqlite->exec([
"insert",
"into" => $channel->getTableName(),
"values" => $row,
]);
} else {
$this->sqlite->exec([
"update",
"table" => $channel->getTableName(),
"values" => $row,
"where" => ["id_" => $id_],
]);
}
return 1;
}
function _discharge(CapacitorChannel $channel, bool $reset=true): iterable {
$rows = $this->sqlite->all([
"select item__",
"from" => $channel->getTableName(),
]);
foreach ($rows as $row) {
yield unserialize($row['item__']);
}
if ($reset) $this->_reset($channel);
}
protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
if ($filter !== null && !is_array($filter)) {
$id = $filter;
$channel->verifixId($id);
$filter = ["id_" => $id];
}
$filter = $this->serialize($channel, $filter);
}
function _count(CapacitorChannel $channel, $filter): int {
$this->verifixFilter($channel, $filter);
return $this->sqlite->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
function _one(CapacitorChannel $channel, $filter): ?array {
if ($filter === null) throw ValueException::null("filter");
$this->verifixFilter($channel, $filter);
$row = $this->sqlite->one([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
]);
return $this->unserialize($channel, $row);
}
function _all(CapacitorChannel $channel, $filter): iterable {
$this->verifixFilter($channel, $filter);
$rows = $this->sqlite->all([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
]);
foreach ($rows as $row) {
yield $this->unserialize($channel, $row);
}
}
function _each(CapacitorChannel $channel, $filter, ?callable $func, ?array $args): int {
if ($func === null) $func = [$channel, "onEach"];
$onEach = func::_prepare($func);
$sqlite = $this->sqlite;
$tableName = $channel->getTableName();
$commited = false;
$count = 0;
$sqlite->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
try {
$args ??= [];
foreach ($this->_all($channel, $filter) as $row) {
$updates = func::_call($onEach, [$row["item"], $row, ...$args]);
if (is_array($updates)) {
$updates = $this->serialize($channel, $updates);
if (array_key_exists("item__", $updates)) {
# si item a été mis à jour, il faut mettre à jour sum_
$updates["sum_"] = sha1($updates["item__"]);
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
}
}
$sqlite->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => ["id_" => $row["id_"]],
]);
if ($commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold == 0) {
$sqlite->commit();
$sqlite->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
$sqlite->commit();
$commited = true;
return $count;
} finally {
if (!$commited) $sqlite->rollback();
}
}
function close(): void { function close(): void {
$this->sqlite->close(); $this->db->close();
} }
} }

View File

@ -1,215 +0,0 @@
<?php
namespace nulib\db\sqlite;
use nulib\cl;
use nulib\str;
use nulib\ValueException;
use SQLite3;
use SQLite3Stmt;
class _query {
static function verifix(&$query, ?array &$params=null): void {
if (is_array($query)) {
$prefix = $query[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_query_create::isa($prefix)) {
$query = _query_create::parse($query, $params);
} elseif (_query_select::isa($prefix)) {
$query = _query_select::parse($query, $params);
} elseif (_query_insert::isa($prefix)) {
$query = _query_insert::parse($query, $params);
} elseif (_query_update::isa($prefix)) {
$query = _query_update::parse($query, $params);
} elseif (_query_delete::isa($prefix)) {
$query = _query_delete::parse($query, $params);
} elseif (_query_generic::isa($prefix)) {
$query = _query_generic::parse($query, $params);
} else {
throw SqliteException::wrap(ValueException::invalid_kind($query, "query"));
}
} elseif (!is_string($query)) {
$query = strval($query);
}
}
protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool {
if (!preg_match("/^$pattern/i", $string, $ms)) return false;
$string = substr($string, strlen($ms[0]));
return true;
}
/** fusionner toutes les parties séquentielles d'une requête */
protected static function merge_seq(array $query): string {
$index = 0;
$sql = "";
foreach ($query as $key => $value) {
if ($key === $index) {
$index++;
if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) {
$sql .= " ";
}
$sql .= $value;
}
}
return $sql;
}
protected static function is_sep(&$cond): bool {
if (!is_string($cond)) return false;
if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false;
$cond = $ms[1];
return true;
}
static function parse_conds(?array $conds, ?array &$sql, ?array &$params): void {
if (!$conds) return;
$sep = null;
$index = 0;
$condsql = [];
foreach ($conds as $key => $cond) {
if ($key === $index) {
## séquentiel
if ($index === 0 && self::is_sep($cond)) {
$sep = $cond;
} elseif (is_array($cond)) {
# condition récursive
self::parse_conds($cond, $condsql, $params);
} else {
# condition litérale
$condsql[] = strval($cond);
}
$index++;
} else {
## associatif
# paramètre
$param = $key;
if ($params !== null && array_key_exists($param, $params)) {
$i = 1;
while (array_key_exists("$key$i", $params)) {
$i++;
}
$param = "$key$i";
}
# value ou [operator, value]
if (is_array($cond)) {
#XXX implémenter le support de ["between", lower, upper]
# et aussi ["in", values]
$op = null;
$value = null;
$condkeys = array_keys($cond);
if (array_key_exists("op", $cond)) $op = $cond["op"];
if (array_key_exists("value", $cond)) $value = $cond["value"];
$condkey = 0;
if ($op === null && array_key_exists($condkey, $condkeys)) {
$op = $cond[$condkeys[$condkey]];
$condkey++;
}
if ($value === null && array_key_exists($condkey, $condkeys)) {
$value = $cond[$condkeys[$condkey]];
$condkey++;
}
} elseif ($cond !== null) {
$op = "=";
$value = $cond;
} else {
$op = "is null";
$value = null;
}
$cond = [$key, $op];
if ($value !== null) {
$cond[] = ":$param";
$params[$param] = $value;
}
$condsql[] = implode(" ", $cond);
}
}
if ($sep === null) $sep = "and";
$count = count($condsql);
if ($count > 1) {
$sql[] = "(" . implode(" $sep ", $condsql) . ")";
} elseif ($count == 1) {
$sql[] = $condsql[0];
}
}
static function parse_set_values(?array $values, ?array &$sql, ?array &$params): void {
if (!$values) return;
$index = 0;
$parts = [];
foreach ($values as $key => $part) {
if ($key === $index) {
## séquentiel
if (is_array($part)) {
# paramètres récursifs
self::parse_set_values($part, $parts, $params);
} else {
# paramètre litéral
$parts[] = strval($part);
}
$index++;
} else {
## associatif
# paramètre
$param = $key;
if ($params !== null && array_key_exists($param, $params)) {
$i = 1;
while (array_key_exists("$key$i", $params)) {
$i++;
}
$param = "$key$i";
}
# value
$value = $part;
$part = [$key, "="];
if ($value === null) {
$part[] = "null";
} else {
$part[] = ":$param";
$params[$param] = $value;
}
$parts[] = implode(" ", $part);
}
}
$sql = cl::merge($sql, $parts);
}
protected static function check_eof(string $tmpsql, string $usersql): void {
self::consume(';\s*', $tmpsql);
if ($tmpsql) {
throw new ValueException("unexpected value at end: $usersql");
}
}
function __construct($sql, ?array $params=null) {
self::verifix($sql, $params);
$this->sql = $sql;
$this->params = $params;
}
/** @var string */
protected $sql;
/** @var ?array */
protected $params;
function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
if ($this->params !== null) {
/** @var SQLite3Stmt $stmt */
$stmt = SqliteException::check($db, $db->prepare($this->sql));
$close = true;
try {
foreach ($this->params as $param => $value) {
SqliteException::check($db, $stmt->bindValue($param, $value));
}
$close = false;
return true;
} finally {
if ($close) $stmt->close();
}
} else {
$sql = $this->sql;
return false;
}
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_base;
use nulib\ValueException;
use SQLite3;
use SQLite3Stmt;
class _query_base extends _base {
protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_query_create::isa($prefix)) {
$sql = _query_create::parse($sql, $bindinds);
} elseif (_query_select::isa($prefix)) {
$sql = _query_select::parse($sql, $bindinds);
} elseif (_query_insert::isa($prefix)) {
$sql = _query_insert::parse($sql, $bindinds);
} elseif (_query_update::isa($prefix)) {
$sql = _query_update::parse($sql, $bindinds);
} elseif (_query_delete::isa($prefix)) {
$sql = _query_delete::parse($sql, $bindinds);
} elseif (_query_generic::isa($prefix)) {
$sql = _query_generic::parse($sql, $bindinds);
} else {
throw SqliteException::wrap(ValueException::invalid_kind($sql, "query"));
}
} elseif (!is_string($sql)) {
$sql = strval($sql);
}
}
const DEBUG_QUERIES = false;
function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) error_log($this->sql); #XXX
if ($this->bindings !== null) {
/** @var SQLite3Stmt $stmt */
$stmt = SqliteException::check($db, $db->prepare($this->sql));
$close = true;
try {
foreach ($this->bindings as $param => $value) {
SqliteException::check($db, $stmt->bindValue($param, $value));
}
$close = false;
return true;
} finally {
if ($close) $stmt->close();
}
} else {
$sql = $this->sql;
return false;
}
}
}

View File

@ -1,47 +1,10 @@
<?php <?php
namespace nulib\db\sqlite; namespace nulib\db\sqlite;
class _query_create extends _query { use nulib\db\_private\_create;
const SCHEMA = [ use nulib\db\_private\Tcreate;
"prefix" => "?string",
"table" => "string",
"schema" => "?array",
"cols" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool { class _query_create extends _query_base {
//return preg_match("/^create(?:\s+table)?\b/i", $sql); use Tcreate;
#XXX implémentation minimale const SCHEMA = _create::SCHEMA;
return preg_match("/^create\s+table\b/i", $sql);
}
static function parse(array $query, ?array &$params=null): string {
#XXX implémentation minimale
$sql = [self::merge_seq($query)];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["table"];
## columns
$cols = $query["cols"];
$index = 0;
foreach ($cols as $col => &$definition) {
if ($col === $index) {
$index++;
} else {
$definition = "$col $definition";
}
}; unset($definition);
$sql[] = "(".implode(", ", $cols).")";
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
} }

View File

@ -1,44 +1,10 @@
<?php <?php
namespace nulib\db\sqlite; namespace nulib\db\sqlite;
class _query_delete extends _query { use nulib\db\_private\_delete;
const SCHEMA = [ use nulib\db\_private\Tdelete;
"prefix" => "?string",
"from" => "?string",
"where" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool { class _query_delete extends _query_base {
//return preg_match("/^delete(?:\s+from)?\b/i", $sql); use Tdelete;
#XXX implémentation minimale const SCHEMA = _delete::SCHEMA;
return preg_match("/^delete\s+from\b/i", $sql);
}
static function parse(array $query, ?array &$params=null): string {
#XXX implémentation minimale
$sql = [self::merge_seq($query)];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["table"];
## where
$where = $query["where"] ?? null;
if ($where !== null) {
_query::parse_conds($where, $wheresql, $params);
if ($wheresql) {
$sql[] = "where";
$sql[] = implode(" and ", $wheresql);
}
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
} }

View File

@ -1,18 +1,10 @@
<?php <?php
namespace nulib\db\sqlite; namespace nulib\db\sqlite;
use nulib\cl; use nulib\db\_private\_generic;
use nulib\ValueException; use nulib\db\_private\Tgeneric;
class _query_generic extends _query { class _query_generic extends _query_base {
static function isa(string $sql): bool { use Tgeneric;
return preg_match('/^(?:drop\s+table)\b/i', $sql); const SCHEMA = _generic::SCHEMA;
}
static function parse(array $query, ?array &$params=null): string {
if (!cl::is_list($query)) {
throw new ValueException("Seuls les tableaux séquentiels sont supportés");
}
return self::merge_seq($query);
}
} }

View File

@ -1,91 +1,10 @@
<?php <?php
namespace nulib\db\sqlite; namespace nulib\db\sqlite;
use nulib\cl; use nulib\db\_private\_insert;
use nulib\ValueException; use nulib\db\_private\Tinsert;
class _query_insert extends _query { class _query_insert extends _query_base {
const SCHEMA = [ use Tinsert;
"prefix" => "?string", const SCHEMA = _insert::SCHEMA;
"into" => "?string",
"schema" => "?array",
"cols" => "?array",
"values" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool {
return preg_match("/^insert\b/i", $sql);
}
/**
* parser une chaine de la forme
* "insert [into] [TABLE] [(COLS)] [values (VALUES)]"
*/
static function parse(array $query, ?array &$params=null): string {
# fusionner d'abord toutes les parties séquentielles
$usersql = $tmpsql = self::merge_seq($query);
### vérifier la présence des parties nécessaires
$sql = [];
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## insert
self::consume('insert\s*', $tmpsql);
$sql[] = "insert";
## into
self::consume('into\s*', $tmpsql);
$sql[] = "into";
$into = $query["into"] ?? null;
if (self::consume('([a-z_][a-z0-9_]*)\s*', $tmpsql, $ms)) {
if ($into === null) $into = $ms[1];
$sql[] = $into;
} elseif ($into !== null) {
$sql[] = $into;
} else {
throw new ValueException("expected table name: $usersql");
}
## cols & values
$usercols = [];
$uservalues = [];
if (self::consume('\(([^)]*)\)\s*', $tmpsql, $ms)) {
$usercols = array_merge($usercols, preg_split("/\s*,\s*/", $ms[1]));
}
$cols = cl::withn($query["cols"] ?? null);
$values = cl::withn($query["values"] ?? null);
$schema = $query["schema"] ?? null;
if ($cols === null) {
if ($usercols) {
$cols = $usercols;
} elseif ($values) {
$cols = array_keys($values);
$usercols = array_merge($usercols, $cols);
} elseif ($schema && is_array($schema)) {
#XXX implémenter support AssocSchema
$cols = array_keys($schema);
$usercols = array_merge($usercols, $cols);
}
}
if (self::consume('values\s+\(\s*(.*)\s*\)\s*', $tmpsql, $ms)) {
if ($ms[1]) $uservalues[] = $ms[1];
}
if ($cols !== null && !$uservalues) {
if (!$usercols) $usercols = $cols;
foreach ($cols as $col) {
$uservalues[] = ":$col";
$params[$col] = $values[$col] ?? null;
}
}
$sql[] = "(" . implode(", ", $usercols) . ")";
$sql[] = "values (" . implode(", ", $uservalues) . ")";
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
self::check_eof($tmpsql, $usersql);
return implode(" ", $sql);
}
} }

View File

@ -1,169 +1,10 @@
<?php <?php
namespace nulib\db\sqlite; namespace nulib\db\sqlite;
use nulib\cl; use nulib\db\_private\_select;
use nulib\ValueException; use nulib\db\_private\Tselect;
class _query_select extends _query { class _query_select extends _query_base {
const SCHEMA = [ use Tselect;
"prefix" => "?string", const SCHEMA = _select::SCHEMA;
"schema" => "?array",
"cols" => "?array",
"from" => "?string",
"where" => "?array",
"order by" => "?array",
"group by" => "?array",
"having" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool {
return preg_match("/^select\b/i", $sql);
}
/**
* parser une chaine de la forme
* "select [COLS] [from TABLE] [where CONDS] [order by ORDERS] [group by GROUPS] [having CONDS]"
*/
static function parse(array $query, ?array &$params=null): string {
# fusionner d'abord toutes les parties séquentielles
$usersql = $tmpsql = self::merge_seq($query);
### vérifier la présence des parties nécessaires
$sql = [];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## select
self::consume('select\s*', $tmpsql);
$sql[] = "select";
## cols
$usercols = [];
if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) {
if ($ms[1]) $usercols[] = $ms[1];
}
$tmpcols = cl::withn($query["cols"] ?? null);
$schema = $query["schema"] ?? null;
if ($tmpcols !== null) {
$cols = [];
$index = 0;
foreach ($tmpcols as $key => $col) {
if ($key === $index) {
$index++;
$cols[] = $col;
$usercols[] = $col;
} else {
$cols[] = $key;
$usercols[] = "$col as $key";
}
}
} else {
$cols = null;
if ($schema && is_array($schema) && !in_array("*", $usercols)) {
$cols = array_keys($schema);
$usercols = array_merge($usercols, $cols);
}
}
if (!$usercols && !$cols) $usercols = ["*"];
$sql[] = implode(" ", $usercols);
## from
$from = $query["from"] ?? null;
if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?=;?\s*$|\bwhere\b)', $tmpsql, $ms)) {
if ($from === null) $from = $ms[1];
$sql[] = "from";
$sql[] = $from;
} elseif ($from !== null) {
$sql[] = "from";
$sql[] = $from;
} else {
throw new ValueException("expected table name: $usersql");
}
## where
$userwhere = [];
if (self::consume('where\b\s*(.*?)(?=;?\s*$|\border\s+by\b)', $tmpsql, $ms)) {
if ($ms[1]) $userwhere[] = $ms[1];
}
$where = cl::withn($query["where"] ?? null);
if ($where !== null) self::parse_conds($where, $userwhere, $params);
if ($userwhere) {
$sql[] = "where";
$sql[] = implode(" and ", $userwhere);
}
## order by
$userorderby = [];
if (self::consume('order\s+by\b\s*(.*?)(?=;?\s*$|\bgroup\s+by\b)', $tmpsql, $ms)) {
if ($ms[1]) $userorderby[] = $ms[1];
}
$orderby = cl::withn($query["order by"] ?? null);
if ($orderby !== null) {
$index = 0;
foreach ($orderby as $key => $value) {
if ($key === $index) {
$userorderby[] = $value;
$index++;
} else {
if ($value === null) $value = false;
if (!is_bool($value)) {
$userorderby[] = "$key $value";
} elseif ($value) {
$userorderby[] = $key;
}
}
}
}
if ($userorderby) {
$sql[] = "order by";
$sql[] = implode(", ", $userorderby);
}
## group by
$usergroupby = [];
if (self::consume('group\s+by\b\s*(.*?)(?=;?\s*$|\bhaving\b)', $tmpsql, $ms)) {
if ($ms[1]) $usergroupby[] = $ms[1];
}
$groupby = cl::withn($query["group by"] ?? null);
if ($groupby !== null) {
$index = 0;
foreach ($groupby as $key => $value) {
if ($key === $index) {
$usergroupby[] = $value;
$index++;
} else {
if ($value === null) $value = false;
if (!is_bool($value)) {
$usergroupby[] = "$key $value";
} elseif ($value) {
$usergroupby[] = $key;
}
}
}
}
if ($usergroupby) {
$sql[] = "group by";
$sql[] = implode(", ", $usergroupby);
}
## having
$userhaving = [];
if (self::consume('having\b\s*(.*?)(?=;?\s*$)', $tmpsql, $ms)) {
if ($ms[1]) $userhaving[] = $ms[1];
}
$having = cl::withn($query["having"] ?? null);
if ($having !== null) self::parse_conds($having, $userhaving, $params);
if ($userhaving) {
$sql[] = "having";
$sql[] = implode(" and ", $userhaving);
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
self::check_eof($tmpsql, $usersql);
return implode(" ", $sql);
}
} }

View File

@ -1,50 +1,10 @@
<?php <?php
namespace nulib\db\sqlite; namespace nulib\db\sqlite;
class _query_update extends _query { use nulib\db\_private\_update;
const SCHEMA = [ use nulib\db\_private\Tupdate;
"prefix" => "?string",
"table" => "?string",
"schema" => "?array",
"cols" => "?array",
"values" => "?array",
"where" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool { class _query_update extends _query_base {
return preg_match("/^update\b/i", $sql); use Tupdate;
} const SCHEMA = _update::SCHEMA;
static function parse(array $query, ?array &$params=null): string {
#XXX implémentation minimale
$sql = [self::merge_seq($query)];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["table"];
## set
_query::parse_set_values($query["values"], $setsql, $params);
$sql[] = "set";
$sql[] = implode(", ", $setsql);
## where
$where = $query["where"] ?? null;
if ($where !== null) {
_query::parse_conds($where, $wheresql, $params);
if ($wheresql) {
$sql[] = "where";
$sql[] = implode(" and ", $wheresql);
}
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
} }

View File

@ -0,0 +1,112 @@
<?php
namespace nulib\ext\spreadsheet;
use nulib\file\csv\AbstractBuilder;
use nulib\file\csv\TAbstractBuilder;
use nulib\os\path;
use nulib\web\http;
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
class PhpSpreadsheetBuilder extends AbstractBuilder {
use TAbstractBuilder;
/** @var string|int|null nom de la feuille dans laquelle écrire */
const WSNAME = null;
function __construct(?string $output, ?array $params=null) {
parent::__construct($output, $params);
$this->ss = new Spreadsheet();
$this->valueBinder = new StringValueBinder();
$this->setWsname($params["wsname"] ?? static::WSNAME);
}
protected Spreadsheet $ss;
protected IValueBinder $valueBinder;
protected ?Worksheet $ws;
protected int $nrow;
const STYLE_ROW = 0, STYLE_HEADER = 1;
protected int $rowStyle;
/**
* @param string|int|null $wsname
*/
function setWsname($wsname): self {
$ss = $this->ss;
$this->ws = null;
$this->nrow = 0;
$this->rowStyle = self::STYLE_ROW;
$ws = wsutils::get_ws($wsname, $ss);
if ($ws === null) {
$ws = $ss->createSheet()->setTitle($wsname);
$this->wroteHeaders = false;
} else {
$maxRow = wsutils::compute_max_coords($ws)[1];
$this->nrow = $maxRow - 1;
$this->wroteHeaders = $maxRow > 1;
}
$this->ws = $ws;
return $this;
}
function _write(array $row): void {
$ws = $this->ws;
$styleHeader = $this->rowStyle === self::STYLE_HEADER;
$nrow = ++$this->nrow;
$ncol = 1;
foreach ($row as $col) {
$ws->getCellByColumnAndRow($ncol++, $nrow)->setValue($col, $this->valueBinder);
}
if ($styleHeader) {
$ws->getStyle("$nrow:$nrow")->getFont()->setBold(true);
$maxcol = count($row);
for ($ncol = 1; $ncol <= $maxcol; $ncol++) {
$ws->getColumnDimensionByColumn($ncol)->setAutoSize(true);
}
}
}
function writeHeaders(?array $headers=null): void {
$this->rowStyle = self::STYLE_HEADER;
parent::writeHeaders($headers);
$this->rowStyle = self::STYLE_ROW;
}
function _sendContentType(): void {
switch (path::ext($this->output)) {
case ".ods":
$contentType = "application/vnd.oasis.opendocument.spreadsheet";
break;
case ".xlsx":
default:
$contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
break;
}
http::content_type($contentType);
}
protected function _checkOk(): bool {
switch (path::ext($this->output)) {
case ".ods":
$writer = new Ods($this->ss);
break;
case ".xlsx":
default:
$writer = new Xlsx($this->ss);
break;
}
$writer->save($this->getResource());
$this->rewind();
return true;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace nulib\ext\spreadsheet;
use nulib\cl;
use nulib\file\csv\AbstractReader;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
class PhpSpreadsheetReader extends AbstractReader {
const DATETIME_FORMAT = 'dd/mm/yyyy hh:mm:ss';
const DATE_FORMAT = 'dd/mm/yyyy';
const TIME_FORMAT = 'hh:mm:ss';
const FORMAT_MAPPINGS = [
'mm/dd hh' => self::DATETIME_FORMAT,
'dd/mm hh' => self::DATETIME_FORMAT,
'mm/dd hh:mm' => self::DATETIME_FORMAT,
'dd/mm hh:mm' => self::DATETIME_FORMAT,
'mm/dd hh:mm:ss' => self::DATETIME_FORMAT,
'dd/mm hh:mm:ss' => self::DATETIME_FORMAT,
'mm/dd/yyyy hh' => self::DATETIME_FORMAT,
'dd/mm/yyyy hh' => self::DATETIME_FORMAT,
'mm/dd/yyyy hh:mm' => self::DATETIME_FORMAT,
'dd/mm/yyyy hh:mm' => self::DATETIME_FORMAT,
'mm/dd/yyyy hh:mm:ss' => self::DATETIME_FORMAT,
'dd/mm/yyyy hh:mm:ss' => self::DATETIME_FORMAT,
'yyyy/mm/dd hh' => self::DATETIME_FORMAT,
'yyyy/mm/dd hh:mm' => self::DATETIME_FORMAT,
'yyyy/mm/dd hh:mm:ss' => self::DATETIME_FORMAT,
'mm/dd' => self::DATE_FORMAT,
'dd/mm' => self::DATE_FORMAT,
'mm/dd/yyyy' => self::DATE_FORMAT,
'dd/mm/yyyy' => self::DATE_FORMAT,
'yyyy/mm/dd' => self::DATE_FORMAT,
'mm/yyyy' => self::DATE_FORMAT,
'hh AM/PM' => self::TIME_FORMAT,
'hh:mm AM/PM' => self::TIME_FORMAT,
'hh:mm:ss AM/PM' => self::TIME_FORMAT,
'hh' => self::TIME_FORMAT,
'hh:mm' => self::TIME_FORMAT,
'hh:mm:ss' => self::TIME_FORMAT,
'[hh]:mm:ss' => self::TIME_FORMAT,
'mm:ss' => self::TIME_FORMAT,
];
/** @var string|int|null nom de la feuille depuis laquelle lire */
const WSNAME = null;
function __construct($input, ?array $params=null) {
parent::__construct($input, $params);
$this->wsname = $params["wsname"] ?? static::WSNAME;
}
protected $wsname;
/**
* @param string|int|null $wsname
*/
function setWsname($wsname): self {
$this->wsname = $wsname;
return $this;
}
function getIterator() {
$ss = IOFactory::load($this->input);
$ws = wsutils::get_ws($this->wsname, $ss);
[$nbCols, $nbRows] = wsutils::compute_max_coords($ws);
$this->isrc = $this->idest = 0;
for ($nrow = 1; $nrow <= $nbRows; $nrow++) {
$row = [];
for ($ncol = 1; $ncol <= $nbCols; $ncol++) {
if ($ws->cellExistsByColumnAndRow($ncol, $nrow)) {
$cell = $ws->getCellByColumnAndRow($ncol, $nrow);
$col = $cell->getValue();
if ($col instanceof RichText) {
$col = $col->getPlainText();
} else {
$dataType = $cell->getDataType();
if ($dataType == DataType::TYPE_NUMERIC || $dataType == DataType::TYPE_FORMULA) {
# si c'est un format date, le forcer à une valeur standard
$origFormatCode = $cell->getStyle()->getNumberFormat()->getFormatCode();
if (strpbrk($origFormatCode, "ymdhs") !== false) {
$formatCode = $origFormatCode;
$formatCode = preg_replace('/y+/', "yyyy", $formatCode);
$formatCode = preg_replace('/m+/', "mm", $formatCode);
$formatCode = preg_replace('/d+/', "dd", $formatCode);
$formatCode = preg_replace('/h+/', "hh", $formatCode);
$formatCode = preg_replace('/s+/', "ss", $formatCode);
$formatCode = preg_replace('/-+/', "/", $formatCode);
$formatCode = preg_replace('/\\\\ /', " ", $formatCode);
$formatCode = preg_replace('/;@$/', "", $formatCode);
$formatCode = cl::get(self::FORMAT_MAPPINGS, $formatCode, $formatCode);
if ($formatCode !== $origFormatCode) {
$cell->getStyle()->getNumberFormat()->setFormatCode($formatCode);
}
}
}
$col = $cell->getFormattedValue();
$this->verifixCol($col);
}
} else {
$col = null;
}
$row[] = $col;
}
if ($this->cook($row)) {
yield $row;
$this->idest++;
}
$this->isrc++;
}
}
}

View File

@ -0,0 +1,202 @@
<?php
namespace nulib\ext\spreadsheet;
use nulib\file\csv\AbstractBuilder;
use nulib\file\csv\TAbstractBuilder;
use nulib\os\path;
use nulib\php\func;
use nulib\php\time\Date;
use nulib\php\time\DateTime;
use nulib\web\http;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Style\Style;
use OpenSpout\Common\Helper\CellTypeHelper;
use OpenSpout\Writer\Common\Creator\WriterEntityFactory;
use OpenSpout\Writer\WriterMultiSheetsAbstract;
use OpenSpout\Writer\XLSX\Entity\SheetView;
class SpoutBuilder extends AbstractBuilder {
use TAbstractBuilder;
const DATE_FORMAT = "mm/dd/yyyy";
const DATETIME_FORMAT = "mm/dd/yyyy hh:mm:ss";
/** @var bool faut-il choisir le type numérique pour une chaine numérique? */
const TYPE_NUMERIC = true;
/** @var bool faut-il choisir le type date pour une chaine au bon format? */
const TYPE_DATE = true;
/** @var string|int|null nom de la feuille dans laquelle écrire */
const WSNAME = null;
function __construct(?string $output, ?array $params=null) {
parent::__construct($output, $params);
$ssType = $params["ss_type"] ?? null;
if ($ssType === null) {
switch (path::ext($this->output)) {
case ".ods":
$ssType = "ods";
break;
case ".xlsx":
default:
$ssType = "xlsx";
break;
}
}
switch ($ssType) {
case "ods":
$ss = WriterEntityFactory::createODSWriter();
break;
case "xlsx":
default:
$ss = WriterEntityFactory::createXLSXWriter();
break;
}
$ss->setDefaultColumnWidth(10.5);
$ss->writeToStream($this->getResource());
$this->ss = $ss;
$this->typeNumeric = boolval($params["type_numeric"] ?? static::TYPE_NUMERIC);
$this->typeDate = boolval($params["type_date"] ?? static::TYPE_DATE);
$this->firstSheet = true;
$this->setWsname($params["wsname"] ?? static::WSNAME);
}
protected WriterMultiSheetsAbstract $ss;
protected bool $typeNumeric;
protected bool $typeDate;
const STYLE_ROW = 0, STYLE_HEADER = 1;
protected int $rowStyle;
protected bool $firstSheet;
/**
* @param string|int|null $wsname
*/
function setWsname($wsname, ?array $params=null): self {
$ss = $this->ss;
$this->rowStyle = self::STYLE_ROW;
if ($this->firstSheet) {
$this->firstSheet = false;
$ws = $ss->getCurrentSheet();
} else {
$ws = $ss->addNewSheetAndMakeItCurrent();
$this->wroteHeaders = false;
$this->built = false;
}
$wsname ??= $params["wsname"] ?? null;
if ($wsname !== null) $ws->setName($wsname);
$sheetView = (new SheetView())
->setFreezeRow(2);
$ws->setSheetView($sheetView);
if ($params !== null) {
if (array_key_exists("schema", $params)) {
$this->schema = $params["schema"] ?? null;
}
if (array_key_exists("headers", $params)) {
$this->headers = $params["headers"] ?? null;
}
if (array_key_exists("rows", $params)) {
$rows = $params["rows"] ?? null;
if (is_callable($rows)) $rows = $rows();
$this->rows = $rows;
}
if (array_key_exists("cook_func", $params)) {
$cookFunc = $params["cook_func"] ?? null;
$cookCtx = $cookArgs = null;
if ($cookFunc !== null) {
func::ensure_func($cookFunc, $this, $cookArgs);
$cookCtx = func::_prepare($cookFunc);
}
$this->cookCtx = $cookCtx;
$this->cookArgs = $cookArgs;
}
if (array_key_exists("type_numeric", $params)) {
$this->typeNumeric = boolval($params["type_numeric"] ?? static::TYPE_NUMERIC);
}
if (array_key_exists("type_date", $params)) {
$this->typeDate = boolval($params["type_date"] ?? static::TYPE_DATE);
}
}
return $this;
}
protected function isNumeric($value): bool {
if ($this->typeNumeric && is_numeric($value)) return true;
if (!is_string($value) && is_numeric($value)) return true;
return false;
}
protected function isDate(&$value, &$style): bool {
if (CellTypeHelper::isDateTimeOrDateInterval($value)) {
$style = (new Style())->setFormat(self::DATE_FORMAT);
return true;
}
if (!is_string($value) || !$this->typeDate) return false;
if (DateTime::isa_datetime($value, true)) {
$value = new DateTime($value);
$style = (new Style())->setFormat(self::DATETIME_FORMAT);
return true;
}
if (DateTime::isa_date($value, true)) {
$value = new Date($value);
$style = (new Style())->setFormat(self::DATE_FORMAT);
return true;
}
return false;
}
function _write(array $row): void {
$cells = [];
$rowStyle = null;
foreach ($row as $col) {
$style = null;
if ($col === null || $col === "") {
$type = Cell::TYPE_EMPTY;
} elseif ($this->isNumeric($col)) {
$type = Cell::TYPE_NUMERIC;
} elseif ($this->isDate($col, $style)) {
$type = Cell::TYPE_DATE;
} else {
$type = Cell::TYPE_STRING;
}
$cell = WriterEntityFactory::createCell($col, $style);
$cell->setType($type);
$cells[] = $cell;
}
if ($this->rowStyle === self::STYLE_HEADER) {
$rowStyle = (new Style())->setFontBold();
}
$this->ss->addRow(WriterEntityFactory::createRow($cells, $rowStyle));
}
function writeHeaders(?array $headers=null): void {
$this->rowStyle = self::STYLE_HEADER;
parent::writeHeaders($headers);
$this->rowStyle = self::STYLE_ROW;
}
function _sendContentType(): void {
switch (path::ext($this->output)) {
case ".ods":
$contentType = "application/vnd.oasis.opendocument.spreadsheet";
break;
case ".xlsx":
default:
$contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
break;
}
http::content_type($contentType);
}
protected function _checkOk(): bool {
$this->ss->close();
$this->rewind();
return true;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace nulib\ext\spreadsheet;
use nulib\cl;
use nulib\file\csv\AbstractReader;
use OpenSpout\Reader\Common\Creator\ReaderEntityFactory;
class SpoutReader extends AbstractReader {
/** @var string|int|null nom de la feuille depuis laquelle lire */
const WSNAME = null;
function __construct($input, ?array $params=null) {
parent::__construct($input, $params);
$this->ssType = $params["ss_type"] ?? null;
$this->allSheets = $params["all_sheets"] ?? true;
$wsname = static::WSNAME;
if ($params !== null && array_key_exists("wsname", $params)) {
# spécifié par l'utilisateur: $allSheets = false
$this->setWsname($params["wsname"]);
} elseif ($wsname !== null) {
# valeur non nulle de la classe: $allSheets = false
$this->setWsname($wsname);
} else {
# pas de valeur définie dans la classe, laisser $allSheets à sa valeur
# actuelle
$this->wsname = null;
}
$this->includeWsnames = cl::withn($params["include_wsnames"] ?? null);
$this->excludeWsnames = cl::withn($params["exclude_wsnames"] ?? null);
}
protected ?string $ssType;
/** @var bool faut-il retourner les lignes de toutes les feuilles? */
protected bool $allSheets;
function setAllSheets(bool $allSheets=true): self {
$this->allSheets = $allSheets;
return $this;
}
/**
* @var array|null si non null, liste de feuilles à inclure. n'est pris en
* compte que si $allSheets===true
*/
protected ?array $includeWsnames;
/**
* @var array|null si non null, liste de feuilles à exclure. n'est pris en
* compte que si $allSheets===true
*/
protected ?array $excludeWsnames;
protected $wsname;
/**
* @param string|int|null $wsname l'unique feuille à sélectionner
*
* NB: appeler cette méthode réinitialise $allSheets à false
*/
function setWsname($wsname): self {
$this->wsname = $wsname;
$this->allSheets = true;
return $this;
}
function getIterator() {
switch ($this->ssType) {
case "ods":
$ss = ReaderEntityFactory::createODSReader();
break;
case "xlsx":
$ss = ReaderEntityFactory::createXLSXReader();
break;
default:
$ss = ReaderEntityFactory::createReaderFromFile($this->input);
break;
}
$ss->open($this->input);
try {
$allSheets = $this->allSheets;
$includeWsnames = $this->includeWsnames;
$excludeWsnames = $this->excludeWsnames;
$wsname = $this->wsname;
$first = true;
foreach ($ss->getSheetIterator() as $ws) {
if ($allSheets) {
$wsname = $ws->getName();
$found = ($includeWsnames === null || in_array($wsname, $includeWsnames))
&& ($excludeWsnames === null || !in_array($wsname, $excludeWsnames));
} else {
$found = $wsname === null || $wsname === $ws->getName();
}
if ($found) {
if ($first) {
$first = false;
} else {
yield null;
# on garde le même schéma le cas échéant, mais supprimer headers
# pour permettre son recalcul
$this->headers = null;
}
$this->isrc = $this->idest = 0;
foreach ($ws->getRowIterator() as $row) {
$row = $row->toArray();
foreach ($row as &$col) {
$this->verifixCol($col);
}; unset($col);
if ($this->cook($row)) {
yield $row;
$this->idest++;
}
$this->isrc++;
}
}
}
} finally {
$ss->close();
}
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace nulib\ext\spreadsheet;
/**
* Class SsBuilder: construction d'une feuille de calcul, pour envoi à
* l'utilisateur
*/
class SsBuilder extends SpoutBuilder {
}

View File

@ -0,0 +1,8 @@
<?php
namespace nulib\ext\spreadsheet;
use nulib\file\csv\TAbstractReader;
class SsReader extends SpoutReader {
use TAbstractReader;
}

View File

@ -0,0 +1,14 @@
<?php
namespace nulib\ext\spreadsheet;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
class ssutils {
static function each_compute_max_coords(Spreadsheet $ss): array {
$max_coords = [];
foreach ($ss->getAllSheets() as $ws) {
$max_coords[$ws->getTitle()] = wsutils::compute_max_coords($ws);
}
return $max_coords;
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace nulib\ext\spreadsheet;
use nulib\ValueException;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class wsutils {
static function get_ws(?string $wsname, Spreadsheet $ss, bool $create=false): ?Worksheet {
if ($wsname == null) {
$ws = $ss->getActiveSheet();
} elseif (is_numeric($wsname)) {
$sheetCount = $ss->getSheetCount();
if ($wsname < 1 || $wsname > $sheetCount) {
throw ValueException::invalid_value($wsname, "sheet index");
}
$ws = $ss->getSheet($wsname - 1);
} else {
$ws = $ss->getSheetByName($wsname);
if ($ws === null) {
if ($create) $ws = $ss->createSheet()->setTitle($wsname);
else throw ValueException::invalid_value($wsname, "sheet name");
}
}
return $ws;
}
static function get_highest_coords(Worksheet $ws): array {
$highestColumnA = $ws->getHighestColumn();
$highestCol = Coordinate::columnIndexFromString($highestColumnA);
$highestRow = $ws->getHighestRow();
return [$highestCol, $highestRow];
}
/**
* @var int nombre de colonnes/lignes au bout desquels on arrête de chercher
* si on n'a trouvé que des cellules vides.
*
* c'est nécessaire à cause de certains fichiers provenant d'Excel que j'ai
* reçus qui ont jusqu'à 10000 colonne vides et/ou 1048576 lignes vides. un
* algorithme "bête" perd énormément de temps à chercher dans le vide, donnant
* l'impression que le processus a planté.
*/
const MAX_EMPTY_THRESHOLD = 150;
static function compute_max_coords(Worksheet $ws): array {
[$highestCol, $highestRow] = self::get_highest_coords($ws);
$maxCol = 1;
$maxRow = 1;
$maxEmptyRows = self::MAX_EMPTY_THRESHOLD;
for ($row = 1; $row <= $highestRow; $row++) {
$emptyRow = true;
$maxEmptyCols = self::MAX_EMPTY_THRESHOLD;
for ($col = 1; $col <= $highestCol; $col++) {
$value = null;
if ($ws->cellExistsByColumnAndRow($col, $row)) {
$value = $ws->getCellByColumnAndRow($col, $row)->getValue();
}
if ($value === null) {
$maxEmptyCols--;
if ($maxEmptyCols == 0) break;
} else {
$maxEmptyCols = self::MAX_EMPTY_THRESHOLD;
if ($row > $maxRow) $maxRow = $row;
if ($col > $maxCol) $maxCol = $col;
$emptyRow = false;
}
}
if ($emptyRow) {
$maxEmptyRows--;
if ($maxEmptyRows == 0) break;
} else {
$maxEmptyRows = self::MAX_EMPTY_THRESHOLD;
}
}
return [$maxCol, $maxRow];
}
}

View File

@ -13,7 +13,7 @@ class FileReader extends _File {
/** @var bool */ /** @var bool */
protected $ignoreBom; protected $ignoreBom;
function __construct($input, ?string $mode=null, bool $throwOnError=true, ?bool $allowLocking=null, ?bool $ignoreBom=null) { function __construct($input, ?string $mode=null, ?bool $throwOnError=null, ?bool $allowLocking=null, ?bool $ignoreBom=null) {
if ($ignoreBom === null) $ignoreBom = static::IGNORE_BOM; if ($ignoreBom === null) $ignoreBom = static::IGNORE_BOM;
$this->ignoreBom = $ignoreBom; $this->ignoreBom = $ignoreBom;
if ($input === null) { if ($input === null) {

View File

@ -10,7 +10,7 @@ use nulib\os\sh;
class FileWriter extends _File { class FileWriter extends _File {
const DEFAULT_MODE = "a+b"; const DEFAULT_MODE = "a+b";
function __construct($output, ?string $mode=null, bool $throwOnError=true, ?bool $allowLocking=null) { function __construct($output, ?string $mode=null, ?bool $throwOnError=null, ?bool $allowLocking=null) {
if ($output === null) { if ($output === null) {
$fd = STDOUT; $fd = STDOUT;
$close = false; $close = false;

View File

@ -17,6 +17,8 @@ interface IReader extends _IFile {
/** @throws IOException */ /** @throws IOException */
function fpassthru(): int; function fpassthru(): int;
function fgetcsv(): ?array;
/** /**
* lire la prochaine ligne. la ligne est retournée *sans* le caractère de fin * lire la prochaine ligne. la ligne est retournée *sans* le caractère de fin
* de ligne [\r]\n * de ligne [\r]\n
@ -26,19 +28,6 @@ interface IReader extends _IFile {
*/ */
function readLine(): ?string; function readLine(): ?string;
/**
* essayer de verrouiller le fichier en lecture. retourner true si l'opération
* réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument
* true
*/
function canRead(): bool;
/**
* verrouiller en mode partagé puis retourner un objet permettant de lire le
* fichier.
*/
function getReader(bool $alreadyLocked=false): IReader;
/** /**
* lire tout le contenu du fichier en une seule fois, puis, si $close==true, * lire tout le contenu du fichier en une seule fois, puis, si $close==true,
* le fermer * le fermer
@ -49,4 +38,6 @@ interface IReader extends _IFile {
/** désérialiser le contenu du fichier, puis, si $close===true, le fermer */ /** désérialiser le contenu du fichier, puis, si $close===true, le fermer */
function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false); function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false);
function copyTo(IWriter $dest, bool $closeWriter=false, bool $closeReader=true): void;
} }

View File

@ -7,31 +7,21 @@ use nulib\os\IOException;
* Interface IWriter: un objet dans lequel on peut écrire des données * Interface IWriter: un objet dans lequel on peut écrire des données
*/ */
interface IWriter extends _IFile { interface IWriter extends _IFile {
/** @throws IOException */
function ftruncate(int $size): self;
/** @throws IOException */ /** @throws IOException */
function fwrite(string $data, int $length=0): int; function fwrite(string $data, int $length=0): int;
/** @throws IOException */
function fputcsv(array $row): void;
/** @throws IOException */ /** @throws IOException */
function fflush(): self; function fflush(): self;
/** @throws IOException */
function ftruncate(int $size): self;
/** afficher les lignes */ /** afficher les lignes */
function writeLines(?iterable $lines): self; function writeLines(?iterable $lines): self;
/**
* essayer de verrouiller le fichier en écriture. retourner true si l'opération
* réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument
* true
*/
function canWrite(): bool;
/**
* verrouiller en mode exclusif puis retourner un objet permettant d'écrire
* dans le fichier
*/
function getWriter(bool $alreadyLocked=false): IWriter;
/** écrire le contenu spécifié dans le fichier */ /** écrire le contenu spécifié dans le fichier */
function putContents(string $contents, bool $close=true, bool $alreadyLocked=false): void; function putContents(string $contents, bool $close=true, bool $alreadyLocked=false): void;

View File

@ -10,7 +10,7 @@ class MemoryStream extends Stream {
return fopen("php://memory", "w+b"); return fopen("php://memory", "w+b");
} }
function __construct(bool $throwOnError=true) { function __construct(?bool $throwOnError=null) {
parent::__construct(self::memory_fd(), true, $throwOnError); parent::__construct(self::memory_fd(), true, $throwOnError);
} }

View File

@ -8,7 +8,7 @@ class SharedFile extends FileWriter {
const DEFAULT_MODE = "c+b"; const DEFAULT_MODE = "c+b";
function __construct($file, ?string $mode=null, bool $throwOnError=true, ?bool $allowLocking=null) { function __construct($file, ?string $mode=null, ?bool $throwOnError=null, ?bool $allowLocking=null) {
if ($file === null) throw ValueException::null("file"); if ($file === null) throw ValueException::null("file");
parent::__construct($file, $mode, $throwOnError, $allowLocking); parent::__construct($file, $mode, $throwOnError, $allowLocking);
} }

View File

@ -16,9 +16,29 @@ use nulib\ValueException;
class Stream extends AbstractIterator implements IReader, IWriter { class Stream extends AbstractIterator implements IReader, IWriter {
use TStreamFilter; use TStreamFilter;
protected static function probe_fd($fd, ?bool &$seekable=null, ?bool &$readable=null): void {
$md = stream_get_meta_data($fd);
$seekable = $md["seekable"];
$mode = $md["mode"];
$readable = strpos($mode, "r") !== false || strpos($mode, "+") !== false;
}
protected static function fd_is_seekable($fd): bool {
self::probe_fd($fd, $seekable);
return $seekable;
}
protected static function fd_is_readable($fd): bool {
$mode = stream_get_meta_data($fd)["mode"];
return strpos($mode, "r") !== false || strpos($mode, "+") !== false;
}
/** @var bool les opérations de verrouillages sont-elle activées? */ /** @var bool les opérations de verrouillages sont-elle activées? */
const USE_LOCKING = false; const USE_LOCKING = false;
/** @var bool faut-il lancer une exception s'il y a une erreur? */
const THROW_ON_ERROR = true;
/** @var resource */ /** @var resource */
protected $fd; protected $fd;
@ -40,13 +60,12 @@ class Stream extends AbstractIterator implements IReader, IWriter {
/** @var array */ /** @var array */
protected $stat; protected $stat;
function __construct($fd, bool $close=true, bool $throwOnError=true, ?bool $useLocking=null) { function __construct($fd, bool $close=true, ?bool $throwOnError=null, ?bool $useLocking=null) {
if ($fd === null) throw ValueException::null("resource"); if ($fd === null) throw ValueException::null("resource");
$this->fd = $fd; $this->fd = $fd;
$this->close = $close; $this->close = $close;
$this->throwOnError = $throwOnError; $this->throwOnError = $throwOnError ?? static::THROW_ON_ERROR;
if ($useLocking === null) $useLocking = static::USE_LOCKING; $this->useLocking = $useLocking ?? static::USE_LOCKING;
$this->useLocking = $useLocking;
} }
############################################################################# #############################################################################
@ -143,24 +162,26 @@ class Stream extends AbstractIterator implements IReader, IWriter {
const DEFAULT_CSV_FLAVOUR = ref_csv::OO_FLAVOUR; const DEFAULT_CSV_FLAVOUR = ref_csv::OO_FLAVOUR;
/** @var array paramètres pour la lecture et l'écriture de flux au format CSV */ /** @var string paramètres pour la lecture et l'écriture de flux au format CSV */
protected $csvFlavour; protected $csvFlavour;
function setCsvFlavour(string $flavour): void { function setCsvFlavour(?string $flavour): void {
$this->csvFlavour = csv_flavours::verifix($flavour); $this->csvFlavour = csv_flavours::verifix($flavour);
} }
protected function getCsvParams($fd): array { protected function getCsvParams($fd): array {
$flavour = $this->csvFlavour; $flavour = $this->csvFlavour;
if ($flavour === null) { if ($flavour === null) {
self::probe_fd($fd, $seekable, $readable);
if (!$seekable || !$readable) $fd = null;
if ($fd === null) { if ($fd === null) {
# utiliser la valeur par défaut # utiliser la valeur par défaut
$flavour = static::DEFAULT_CSV_FLAVOUR; $flavour = static::DEFAULT_CSV_FLAVOUR;
} else { } else {
# il faut déterminer le type de fichier CSV en lisant la première ligne # il faut déterminer le type de fichier CSV en lisant la première ligne
$pos = IOException::ensure_valid(ftell($fd)); $pos = IOException::ensure_valid(ftell($fd));
$line = IOException::ensure_valid(fgets($fd)); $line = fgets($fd);
$line = strpbrk($line, ",;\t"); if ($line !== false) $line = strpbrk($line, ",;\t");
if ($line === false) { if ($line === false) {
# aucun séparateur trouvé, prender la valeur par défaut # aucun séparateur trouvé, prender la valeur par défaut
$flavour = static::DEFAULT_CSV_FLAVOUR; $flavour = static::DEFAULT_CSV_FLAVOUR;
@ -259,13 +280,14 @@ class Stream extends AbstractIterator implements IReader, IWriter {
return new class($this->fd, ++$this->serial, $this) extends Stream { return new class($this->fd, ++$this->serial, $this) extends Stream {
function __construct($fd, int $serial, Stream $parent) { function __construct($fd, int $serial, Stream $parent) {
$this->parent = $parent; $this->parent = $parent;
$this->serial = $serial;
parent::__construct($fd); parent::__construct($fd);
} }
/** @var Stream */ /** @var Stream */
private $parent; private $parent;
function close(bool $close=true): void { function close(bool $close=true, ?int $ifSerial=null): void {
if ($this->parent !== null && $close) { if ($this->parent !== null && $close) {
$this->parent->close(true, $this->serial); $this->parent->close(true, $this->serial);
$this->fd = null; $this->fd = null;
@ -293,13 +315,18 @@ class Stream extends AbstractIterator implements IReader, IWriter {
return unserialize(...$args); return unserialize(...$args);
} }
function decodeJson(bool $close=true, bool $alreadyLocked=false) {
$contents = $this->getContents($close, $alreadyLocked);
return json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
}
############################################################################# #############################################################################
# Iterator # Iterator
protected function _setup(): void { protected function iter_setup(): void {
} }
protected function _next(&$key) { protected function iter_next(&$key) {
try { try {
return $this->fgets(); return $this->fgets();
} catch (EOFException $e) { } catch (EOFException $e) {
@ -307,14 +334,33 @@ class Stream extends AbstractIterator implements IReader, IWriter {
} }
} }
protected function _teardown(): void { private function _rewindFd(): void {
$md = stream_get_meta_data($this->fd); self::probe_fd($this->fd, $seekable);
if ($md["seekable"]) $this->fseek(0); if ($seekable) $this->fseek(0);
}
protected function iter_teardown(): void {
$this->_rewindFd();
}
function rewind(): void {
# il faut toujours faire un rewind sur la resource, que l'itérateur aie été
# initialisé ou non
if ($this->_hasIteratorBeenSetup()) parent::rewind();
else $this->_rewindFd();
} }
############################################################################# #############################################################################
# Writer # Writer
/** @throws IOException */
function ftruncate(int $size=0, bool $rewind=true): self {
$fd = $this->getResource();
IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError);
if ($rewind) rewind($fd);
return $this;
}
/** @throws IOException */ /** @throws IOException */
function fwrite(string $data, ?int $length=null): int { function fwrite(string $data, ?int $length=null): int {
$fd = $this->getResource(); $fd = $this->getResource();
@ -323,10 +369,20 @@ class Stream extends AbstractIterator implements IReader, IWriter {
return IOException::ensure_valid($r, $this->throwOnError); return IOException::ensure_valid($r, $this->throwOnError);
} }
/** @throws IOException */
function fputcsv(array $row): void { function fputcsv(array $row): void {
$fd = $this->getResource(); $fd = $this->getResource();
$params = $this->getCsvParams($fd); $params = $this->getCsvParams($fd);
IOException::ensure_valid(fputcsv($fd, $row, $params[0], $params[1], $params[2])); if (csv_flavours::is_dumb($this->csvFlavour, $sep)) {
$line = [];
foreach ($row as $col) {
$line[] = strval($col);
}
$line = implode($sep, $line);
IOException::ensure_valid(fwrite($fd, "$line\n"), $this->throwOnError);
} else {
IOException::ensure_valid(fputcsv($fd, $row, $params[0], $params[1], $params[2]), $this->throwOnError);
}
} }
/** @throws IOException */ /** @throws IOException */
@ -336,14 +392,6 @@ class Stream extends AbstractIterator implements IReader, IWriter {
return $this; return $this;
} }
/** @throws IOException */
function ftruncate(int $size=0, bool $rewind=true): self {
$fd = $this->getResource();
IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError);
if ($rewind) rewind($fd);
return $this;
}
function writeLines(?iterable $lines): IWriter { function writeLines(?iterable $lines): IWriter {
if ($lines !== null) { if ($lines !== null) {
foreach ($lines as $line) { foreach ($lines as $line) {
@ -388,7 +436,7 @@ class Stream extends AbstractIterator implements IReader, IWriter {
/** @var Stream */ /** @var Stream */
private $parent; private $parent;
function close(bool $close=true): void { function close(bool $close=true, ?int $ifSerial=null): void {
if ($this->parent !== null && $close) { if ($this->parent !== null && $close) {
$this->parent->close(true, $this->serial); $this->parent->close(true, $this->serial);
$this->fd = null; $this->fd = null;
@ -412,4 +460,17 @@ class Stream extends AbstractIterator implements IReader, IWriter {
function serialize($object, bool $close=true, bool $alreadyLocked=false): void { function serialize($object, bool $close=true, bool $alreadyLocked=false): void {
$this->putContents(serialize($object), $close, $alreadyLocked); $this->putContents(serialize($object), $close, $alreadyLocked);
} }
function encodeJson($data, bool $close=true, bool $alreadyLocked=false): void {
$contents = json_encode($data, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE);
$this->putContents($contents, $close, $alreadyLocked);
}
/**
* annuler une tentative d'écriture commencée avec {@link self::canWrite()}
*/
function cancelWrite(bool $close=true): void {
if ($this->useLocking) $this->unlock($close);
elseif ($close) $this->close();
}
} }

View File

@ -9,9 +9,8 @@ namespace nulib\file;
class TempStream extends Stream { class TempStream extends Stream {
const MAX_MEMORY = 2 * 1024 * 1024; const MAX_MEMORY = 2 * 1024 * 1024;
function __construct(?int $maxMemory=null, bool $throwOnError=true) { function __construct(?int $maxMemory=null, ?bool $throwOnError=null) {
if ($maxMemory === null) $maxMemory = static::MAX_MEMORY; $this->maxMemory = $maxMemory ?? static::MAX_MEMORY;
$this->maxMemory = $maxMemory;
parent::__construct($this->tempFd(), true, $throwOnError); parent::__construct($this->tempFd(), true, $throwOnError);
} }

View File

@ -10,7 +10,7 @@ use nulib\os\path;
class TmpfileWriter extends FileWriter { class TmpfileWriter extends FileWriter {
const DEFAULT_MODE = "w+b"; const DEFAULT_MODE = "w+b";
function __construct(?string $destdir=null, ?string $mode=null, bool $throwOnError=true, ?bool $allowLocking=null) { function __construct(?string $destdir=null, ?string $mode=null, ?bool $throwOnError=null, ?bool $allowLocking=null) {
$tmpDir = sys_get_temp_dir(); $tmpDir = sys_get_temp_dir();
if ($destdir === null) $destdir = $tmpDir; if ($destdir === null) $destdir = $tmpDir;
if (is_dir($destdir)) { if (is_dir($destdir)) {
@ -39,6 +39,12 @@ class TmpfileWriter extends FileWriter {
/** @var bool */ /** @var bool */
protected $delete; protected $delete;
/** désactiver la suppression automatique du fichier temporaire */
function keep(): self {
$this->delete = false;
return $this;
}
function __destruct() { function __destruct() {
$this->close(); $this->close();
if ($this->delete) $this->delete(); if ($this->delete) $this->delete();

View File

@ -5,10 +5,6 @@ use nulib\os\IOException;
use nulib\web\http; use nulib\web\http;
abstract class _File extends Stream { abstract class _File extends Stream {
function __construct($fd, bool $close, bool $throwOnError=true, ?bool $allowLocking=null) {
parent::__construct($fd, $close, $throwOnError, $allowLocking);
}
/** @var string */ /** @var string */
protected $file; protected $file;

View File

@ -1,140 +0,0 @@
<?php
namespace nulib\file\app;
use nulib\cl;
use nulib\file\SharedFile;
use nulib\os\path;
use nulib\php\time\DateTime;
use nulib\str;
/**
* Class RunFile: une classe permettant de suivre le fonctionnement d'une
* application qui tourne en tâche de fond
*/
class RunFile {
const RUN_EXT = ".run";
const LOCK_EXT = ".lock";
const NAME = null;
function __construct(string $file, ?string $name=null) {
$file = path::ensure_ext($file, self::RUN_EXT);
$this->file = new SharedFile($file);
$this->name = $name ?? static::NAME;
}
/** @var SharedFile */
protected $file;
/** @var ?string */
protected $name;
protected static function merge(array $data, array $merge): array {
return cl::merge($data, [
"serial" => $data["serial"] + 1,
], $merge);
}
protected function initData(bool $withDateStart=true): array {
$dateStart = $withDateStart? new DateTime(): null;
return [
"name" => $this->name,
"id" => bin2hex(random_bytes(16)),
"pid" => posix_getpid(),
"serial" => 0,
"date_start" => $dateStart,
"date_stop" => null,
"exitcode" => null,
"action" => null,
"action_date_start" => null,
"action_current_step" => null,
"action_max_step" => null,
"action_date_step" => null,
];
}
function read(): array {
$data = $this->file->unserialize();
if (!is_array($data)) $data = $this->initData(false);
return $data;
}
/** tester si l'application est démarrée */
function isStarted(): bool {
$data = $this->read();
return $data["date_start"] !== null;
}
/** tester si l'application est arrêtée */
function isStopped(): bool {
$data = $this->read();
return $data["date_stop"] !== null;
}
function haveWorked(int $serial, ?int &$currentSerial=null): bool {
$data = $this->read();
return $serial !== $data["serial"];
}
protected function willWrite(): array {
$file = $this->file;
$file->lockWrite();
$data = $file->unserialize(null, false, true);
if (!is_array($data)) {
$data = $this->initData();
$file->ftruncate();
$file->serialize($data, false, true);
}
$file->ftruncate();
return [$file, $data];
}
/** indiquer que l'application démarre */
function start(): void {
$this->file->serialize($this->initData());
}
/** indiquer le début d'une action */
function action(?string $title, ?int $maxSteps=null): void {
[$file, $data] = $this->willWrite();
$file->serialize(self::merge($data, [
"action" => $title,
"action_date_start" => new DateTime(),
"action_max_step" => $maxSteps,
"action_current_step" => 0,
]));
}
/** indiquer qu'une étape est franchie dans l'action en cours */
function step(int $nbSteps=1): void {
[$file, $data] = $this->willWrite();
$file->serialize(self::merge($data, [
"action_date_step" => new DateTime(),
"action_current_step" => $data["action_current_step"] + $nbSteps,
]));
}
/** indiquer que l'application s'arrête */
function stop(): void {
[$file, $data] = $this->willWrite();
$file->serialize(self::merge($data, [
"date_stop" => new DateTime(),
]));
}
/** après l'arrêt de l'application, mettre à jour le code de retour */
function stopped(int $exitcode): void {
[$file, $data] = $this->willWrite();
$file->serialize(self::merge($data, [
"exitcode" => $exitcode,
]));
}
function getLockFile(?string $name=null, ?string $title=null): LockFile {
$ext = self::LOCK_EXT;
if ($name !== null) $ext = ".$name$ext";
$file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT);
$name = str::join("/", [$this->name, $name]);
return new LockFile($file, $name, $title);
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace nulib\file\csv;
use DateTimeInterface;
use nulib\cl;
use nulib\file\TempStream;
use nulib\os\path;
use nulib\php\func;
use nulib\php\time\DateTime;
use nulib\web\http;
abstract class AbstractBuilder extends TempStream implements IBuilder {
/** @var ?array schéma des données à écrire */
const SCHEMA = null;
/** @var ?array liste des colonnes à écrire */
const HEADERS = null;
/** @var ?string nom du fichier téléchargé */
const OUTPUT = null;
function __construct(?string $output, ?array $params=null) {
if ($output !== null) $params["output"] = $output;
$this->schema = $params["schema"] ?? static::SCHEMA;
$this->headers = $params["headers"] ?? static::HEADERS;
$rows = $params["rows"] ?? null;
if (is_callable($rows)) $rows = $rows();
$this->rows = $rows;
$cookFunc = $params["cook_func"] ?? null;
$cookCtx = $cookArgs = null;
if ($cookFunc !== null) {
func::ensure_func($cookFunc, $this, $cookArgs);
$cookCtx = func::_prepare($cookFunc);
}
$this->cookCtx = $cookCtx;
$this->cookArgs = $cookArgs;
$this->output = $params["output"] ?? static::OUTPUT;
$maxMemory = $params["max_memory"] ?? null;
$throwOnError = $params["throw_on_error"] ?? null;
parent::__construct($maxMemory, $throwOnError);
}
protected ?array $schema;
protected ?array $headers;
protected ?iterable $rows;
protected ?string $output;
protected ?array $cookCtx;
protected ?array $cookArgs;
protected function ensureHeaders(?array $row=null): void {
if ($this->headers !== null) return;
if ($this->schema === null) $headers = null;
else $headers = array_keys($this->schema);
if ($headers === null && $row !== null) $headers = array_keys($row);
$this->headers = $headers;
}
protected abstract function _write(array $row): void;
protected bool $wroteHeaders = false;
function writeHeaders(?array $headers=null): void {
if ($this->wroteHeaders) return;
if ($headers !== null) $this->headers = $headers;
else $this->ensureHeaders();
if ($this->headers !== null) $this->_write($this->headers);
$this->wroteHeaders = true;
}
protected function cookRow(?array $row): ?array {
if ($this->cookCtx !== null) {
$args = cl::merge([$row], $this->cookArgs);
$row = func::_call($this->cookCtx, $args);
}
if ($row !== null) {
foreach ($row as &$value) {
# formatter les dates
if ($value instanceof DateTime) {
$value = $value->format();
} elseif ($value instanceof DateTimeInterface) {
$value = DateTime::with($value)->format();
}
}; unset($value);
}
return $row;
}
function write(?array $row): void {
$row = $this->cookRow($row);
if ($row === null) return;
$this->writeHeaders(array_keys($row));
$this->_write($row);
}
function writeAll(?iterable $rows=null): void {
$unsetRows = false;
if ($rows === null) {
$rows = $this->rows;
$unsetRows = true;
}
if ($rows !== null) {
foreach ($rows as $row) {
$this->write(cl::with($row));
}
}
if ($unsetRows) $this->rows = null;
}
abstract protected function _sendContentType(): void;
protected bool $sentHeaders = false;
function sendHeaders(): void {
if ($this->sentHeaders) return;
$this->_sendContentType();
$output = $this->output;
if ($output !== null) {
http::download_as(path::filename($output));
}
$this->sentHeaders = true;
}
protected function _build(?iterable $rows=null): void {
$this->writeAll($rows);
$this->writeHeaders();
}
abstract protected function _checkOk(): bool;
protected bool $built = false, $closed = false;
function build(?iterable $rows=null, bool $close=true): bool {
$ok = true;
if (!$this->built) {
$this->_build($rows);
$this->built = true;
}
if ($close && !$this->closed) {
$ok = $this->_checkOk();
$this->closed = true;
}
return $ok;
}
function sendFile(?iterable $rows=null): int {
if (!$this->built) {
$this->_build($rows);
$this->built = true;
}
if (!$this->closed) {
if (!$this->_checkOk()) return 0;
$this->closed = true;
}
$this->sendHeaders();
return $this->fpassthru();
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace nulib\file\csv;
use nulib\A;
use nulib\php\time\Date;
use nulib\php\time\DateTime;
abstract class AbstractReader implements IReader {
const SCHEMA = null;
const HEADERS = null;
/** @var ?string nom du fichier depuis lequel lire */
const INPUT = null;
/** @var bool faut-il trimmer le champ avant de le traiter? */
const TRIM = true;
/** @var bool faut-il considérer les chaines vides comme null? */
const PARSE_EMPTY_AS_NULL = true;
/**
* @var bool faut-il forcer le type numérique pour une chaine numérique?
* si false, ne forcer le type numérique que si la chaine ne commence pas zéro
* i.e "06" est une chaine, alors "63" est un nombre
*/
const PARSE_NUMERIC = false;
/**
* @var bool faut-il forcer le type {@link Date} ou {@link DateTime} pour une
* chaine au bon format?
*/
const PARSE_DATE = true;
function __construct($input, ?array $params=null) {
if ($input !== null) $params["input"] = $input;
#
$this->schema = $params["schema"] ?? static::SCHEMA;
$this->headers = $params["headers"] ?? static::HEADERS;
$this->input = $params["input"] ?? static::INPUT;
$this->trim = boolval($params["trim"] ?? static::TRIM);
$this->parseEmptyAsNull = boolval($params["empty_as_null"] ?? static::PARSE_EMPTY_AS_NULL);
$this->parseNumeric = boolval($params["parse_numeric"] ?? static::PARSE_NUMERIC);
$this->parseDate = boolval($params["parse_date"] ?? static::PARSE_DATE);
}
protected ?array $schema;
protected ?array $headers;
protected $input;
protected bool $trim;
protected bool $parseEmptyAsNull;
protected bool $parseNumeric;
protected bool $parseDate;
protected int $isrc = 0, $idest = 0;
protected function cook(array &$row): bool {
if ($this->isrc == 0) {
# ligne d'en-tête
$headers = $this->headers;
if ($headers === null) {
if ($this->schema === null) $headers = null;
else $headers = array_keys($this->schema);
if ($headers === null) $headers = $row;
$this->headers = $headers;
}
return false;
}
A::ensure_size($row, count($this->headers));
$row = array_combine($this->headers, $row);
return true;
}
protected function verifixCol(&$col): void {
if ($this->trim && is_string($col)) {
$col = trim($col);
}
if ($this->parseEmptyAsNull && $col === "") {
# valeur vide --> null
$col = null;
}
if (!is_string($col)) return;
if ($this->parseDate) {
if (DateTime::isa_datetime($col, true)) {
# datetime
$col = new DateTime($col);
} elseif (DateTime::isa_date($col, true)) {
# date
$col = new Date($col);
}
if (!is_string($col)) return;
}
$parseNumeric = $this->parseNumeric || substr($col, 0, 1) !== "0";
if ($parseNumeric) {
$tmp = str_replace(",", ".", $col);
$float = strpos($tmp, ".") !== false;
if (is_numeric($tmp)) {
if ($float) $col = floatval($tmp);
else $col = intval($tmp);
}
}
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace nulib\file\csv;
use nulib\web\http;
/**
* Class CsvBuilder: construction d'un fichier CSV, pour envoi à l'utilisateur
*/
class CsvBuilder extends AbstractBuilder {
use TAbstractBuilder;
function __construct(?string $output, ?array $params=null) {
$csvFlavour = $params["csv_flavour"] ?? null;
$this->csvFlavour = csv_flavours::verifix($csvFlavour);
parent::__construct($output, $params);
}
protected function _write(array $row): void {
$this->fputcsv($row);
}
function _sendContentType(): void {
http::content_type("text/csv");
}
protected function _checkOk(): bool {
$size = $this->ftell();
if ($size === 0) return false;
$this->rewind();
return true;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace nulib\file\csv;
use nulib\file\FileReader;
class CsvReader extends AbstractReader {
use TAbstractReader;
function __construct($input, ?array $params=null) {
parent::__construct($input, $params);
$this->csvFlavour = $params["csv_flavour"] ?? null;
$this->inputEncoding = $params["input_encoding"] ?? null;
}
protected ?string $csvFlavour;
protected ?string $inputEncoding;
function getIterator() {
$reader = new FileReader($this->input);
$inputEncoding = $this->inputEncoding;
if ($inputEncoding !== null) {
$reader->appendFilter("convert.iconv.$inputEncoding.utf-8");
}
$reader->setCsvFlavour($this->csvFlavour);
while (($row = $reader->fgetcsv()) !== null) {
foreach ($row as &$col) {
$this->verifixCol($col);
}; unset($col);
if ($this->cook($row)) {
yield $row;
$this->idest++;
}
$this->isrc++;
}
$reader->close();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace nulib\file\csv;
interface IBuilder {
function writeHeaders(?array $headers=null): void;
function write(?array $row): void;
function writeAll(?iterable $rows=null): void;
function sendHeaders(): void;
function sendFile(?iterable $rows=null): int;
}

View File

@ -0,0 +1,7 @@
<?php
namespace nulib\file\csv;
use IteratorAggregate;
interface IReader extends IteratorAggregate {
}

View File

@ -0,0 +1,57 @@
<?php
namespace nulib\file\csv;
use nulib\cl;
use nulib\file\web\Upload;
use nulib\os\path;
use nulib\ValueException;
trait TAbstractBuilder {
/** @param Upload|string|array $builder */
static function with($builder, ?array $params=null): IBuilder {
if ($builder instanceof self) return $builder;
$class = null;
if ($builder instanceof Upload) {
# faire un builder dans le même format que le fichier uploadé
if ($builder->isExt(".csv")) {
$class = CsvBuilder::class;
} else {
$class = static::class;
if ($builder->isExt(".ods")) {
$params["ss_type"] = "ods";
} else {
$params["ss_type"] = "xlsx";
}
}
return new $class($builder->name, $params);
}
if (is_string($builder)) {
$params["output"] = $builder;
} elseif (is_array($builder)) {
$params = cl::merge($builder, $params);
} elseif ($builder !== null) {
throw ValueException::invalid_type($builder, self::class);
}
$output = $params["output"] ?? null;
$ssType = null;
if (is_string($output)) {
switch (path::ext($output)) {
case ".csv":
$class = CsvBuilder::class;
break;
case ".ods":
$ssType = "ods";
break;
case ".xlsx":
default:
$ssType = "xlsx";
break;
}
}
$params["ss_type"] = $ssType;
if ($class === null) $class = static::class;
return new $class(null, $params);
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace nulib\file\csv;
use nulib\cl;
use nulib\file\web\Upload;
use nulib\os\path;
use nulib\ValueException;
trait TAbstractReader {
/** @param Upload|string|array $reader */
static function with($reader, ?array $params=null): IReader {
if ($reader instanceof self) return $reader;
$class = null;
if ($reader instanceof Upload) {
if ($reader->isExt(".csv")) {
$class = CsvReader::class;
} else {
$class = static::class;
if ($reader->isExt(".ods")) {
$params["ss_type"] = "ods";
} else {
$params["ss_type"] = "xlsx";
}
}
return new $class($reader->tmpName, $params);
}
if (is_string($reader)) {
$params["input"] = $reader;
} elseif (is_array($reader)) {
$params = cl::merge($reader, $params);
} elseif ($reader !== null) {
throw ValueException::invalid_type($reader, self::class);
}
$input = $params["input"] ?? null;
$ssType = null;
if (is_string($input)) {
switch (path::ext($input)) {
case ".csv":
$class = CsvReader::class;
break;
case ".ods":
$ssType = "ods";
break;
case ".xlsx":
default:
$ssType = "xlsx";
break;
}
}
$params["ss_type"] = $ssType;
if ($class === null) $class = static::class;
return new $class(null, $params);
}
}

View File

@ -3,23 +3,29 @@ namespace nulib\file\csv;
use nulib\cl; use nulib\cl;
use nulib\ref\file\csv\ref_csv; use nulib\ref\file\csv\ref_csv;
use nulib\str;
class csv_flavours { class csv_flavours {
const MAP = [ const MAP = [
"oo" => ref_csv::OO_FLAVOUR, "oo" => ref_csv::OO_FLAVOUR,
"ooffice" => ref_csv::OO_FLAVOUR, "ooffice" => ref_csv::OO_FLAVOUR,
ref_csv::OO_NAME => ref_csv::OO_FLAVOUR, ref_csv::OOCALC => ref_csv::OO_FLAVOUR,
"xl" => ref_csv::XL_FLAVOUR, "xl" => ref_csv::XL_FLAVOUR,
"excel" => ref_csv::XL_FLAVOUR, "excel" => ref_csv::XL_FLAVOUR,
ref_csv::XL_NAME => ref_csv::XL_FLAVOUR, ref_csv::MSEXCEL => ref_csv::XL_FLAVOUR,
"dumb;" => ref_csv::DUMB_XL_FLAVOUR,
"dumb," => ref_csv::DUMB_OO_FLAVOUR,
"dumb" => ref_csv::DUMB_FLAVOUR,
]; ];
const ENCODINGS = [ const ENCODINGS = [
ref_csv::OO_FLAVOUR => ref_csv::OO_ENCODING, ref_csv::OO_FLAVOUR => ref_csv::OO_ENCODING,
ref_csv::XL_FLAVOUR => ref_csv::XL_ENCODING, ref_csv::XL_FLAVOUR => ref_csv::XL_ENCODING,
ref_csv::DUMB_FLAVOUR => ref_csv::DUMB_ENCODING,
]; ];
static final function verifix(string $flavour): string { static final function verifix(?string $flavour): ?string {
if ($flavour === null) return null;
$lflavour = strtolower($flavour); $lflavour = strtolower($flavour);
if (array_key_exists($lflavour, self::MAP)) { if (array_key_exists($lflavour, self::MAP)) {
$flavour = self::MAP[$lflavour]; $flavour = self::MAP[$lflavour];
@ -31,8 +37,8 @@ class csv_flavours {
} }
static final function get_name(string $flavour): string { static final function get_name(string $flavour): string {
if ($flavour == ref_csv::OO_FLAVOUR) return ref_csv::OO_NAME; if ($flavour == ref_csv::OO_FLAVOUR) return ref_csv::OOCALC;
elseif ($flavour == ref_csv::XL_FLAVOUR) return ref_csv::XL_NAME; elseif ($flavour == ref_csv::XL_FLAVOUR) return ref_csv::MSEXCEL;
else return $flavour; else return $flavour;
} }
@ -43,4 +49,11 @@ class csv_flavours {
static final function get_encoding(string $flavour): ?string { static final function get_encoding(string $flavour): ?string {
return cl::get(self::ENCODINGS, $flavour); return cl::get(self::ENCODINGS, $flavour);
} }
static final function is_dumb(string $flavour, ?string &$sep): bool {
if (!str::del_prefix($flavour, "xxx")) return false;
$sep = $flavour;
if (!$sep) $sep = ";";
return true;
}
} }

View File

@ -1,7 +1,9 @@
<?php <?php
namespace nulib\file\web; namespace nulib\file\web;
use nulib\cl;
use nulib\file\FileReader; use nulib\file\FileReader;
use nulib\os\path;
use nulib\php\coll\BaseArray; use nulib\php\coll\BaseArray;
use nulib\ValueException; use nulib\ValueException;
@ -84,6 +86,18 @@ class Upload extends BaseArray {
return $this->error === UPLOAD_ERR_OK; return $this->error === UPLOAD_ERR_OK;
} }
/**
* retourner true si le nom du fichier a une des extensions de $exts
*
* @param string|array $exts une ou plusieurs extensions qui sont vérifiées
*/
function isExt($exts): bool {
if ($exts === null) return false;
$ext = path::ext($this->name);
$exts = cl::with($exts);
return in_array($ext, $exts);
}
/** @var ?string chemin du fichier, s'il a été déplacé */ /** @var ?string chemin du fichier, s'il a été déplacé */
protected $file; protected $file;

View File

@ -156,7 +156,11 @@ class path {
return $basename; return $basename;
} }
/** obtenir l'extension du fichier. l'extension est retournée avec le '.' */ /**
* obtenir l'extension du fichier. l'extension est retournée avec le '.'
*
* si le fichier n'a pas d'extension, retourner une chaine vide
*/
static final function ext($path): ?string { static final function ext($path): ?string {
if ($path === null || $path === false) return null; if ($path === null || $path === false) return null;
$ext = self::filename($path); $ext = self::filename($path);

View File

@ -0,0 +1,201 @@
<?php
namespace nulib\os\proc;
use nulib\A;
use nulib\cv;
use nulib\os\sh;
abstract class AbstractCmd implements ICmd {
private bool $needsStdin;
private bool $needsTty;
protected ?array $sources;
protected ?array $vars;
protected array $cmds;
function __construct() {
$this->needsStdin = true;
$this->needsTty = true;
$this->sources = null;
$this->vars = null;
$this->cmds = [];
}
function then($cmd, ?string $input=null, ?string $output=null): Cmd {
if ($this instanceof Cmd) {
$this->add($cmd, $input, $output);
return $this;
} else {
return (new Cmd($this))->add($cmd, $input, $output);
}
}
function or($cmd, ?string $input=null, ?string $output=null): CmdOr {
if ($this instanceof CmdOr) {
$this->add($cmd, $input, $output);
return $this;
} else {
return (new CmdOr($this))->add($cmd, $input, $output);
}
}
function and($cmd, ?string $input=null, ?string $output=null): CmdAnd {
if ($this instanceof CmdAnd) {
$this->add($cmd, $input, $output);
return $this;
} else {
return (new CmdAnd($this))->add($cmd, $input, $output);
}
}
function pipe($cmd): CmdPipe {
if ($this instanceof CmdPipe) {
$this->add($cmd);
return $this;
} else {
return new CmdPipe([$this, $cmd]);
}
}
function isNeedsStdin(): bool {
return $this->needsStdin;
}
function setNeedsStdin(bool $needsStdin): void {
$this->needsStdin = $needsStdin;
}
function isNeedsTty(): bool {
return $this->needsTty;
}
function setNeedsTty(bool $needsTty): void {
$this->needsTty = $needsTty;
}
function addSource(?string $source, bool $onlyIfExists=true): void {
if ($source === null) return;
if (!$onlyIfExists || file_exists($source)) {
$source = implode(" ", [".", sh::quote($source)]);
$this->sources[] = $source;
}
}
function getSources(?string $sep=null): ?string {
if ($this->sources === null) return null;
if ($sep === null) $sep = "\n";
return implode($sep, $this->sources);
}
function addLiteralVars($vars, ?string $sep=null): void {
if (cv::z($vars)) return;
if (is_array($vars)) {
if ($sep === null) $sep = "\n";
$vars = implode($sep, $vars);
}
$this->vars[] = strval($vars);
}
function addVars(?array $vars): void {
if ($vars === null) return;
foreach ($vars as $name => $value) {
$var = [];
if (!is_array($value)) $var[] = "export ";
A::merge($var, [$name, "=", sh::quote($value)]);
$this->vars[] = implode("", $var);
}
}
function getVars(?string $sep=null): ?string {
if ($this->vars === null) return null;
if ($sep === null) $sep = "\n";
return implode($sep, $this->vars);
}
function addPrefix($prefix): void {
$count = count($this->cmds);
if ($count == 0) return;
$cmd =& $this->cmds[$count - 1];
if ($cmd instanceof ICmd) {
$cmd->addPrefix($prefix);
} elseif (is_array($prefix)) {
$prefix = sh::join($prefix);
$cmd = "$prefix $cmd";
} else {
$cmd = "$prefix $cmd";
}
}
function addRedir(?string $redir, $output=null, bool $append=false, $input=null): void {
$count = count($this->cmds);
if ($count == 0) return;
if ($output !== null) $output = escapeshellarg($output);
if ($input !== null) $input = escapeshellarg($input);
if ($redir === "default") $redir = null;
$gt = $append? ">>": ">";
if ($redir === null) {
$redirs = [];
if ($input !== null) $redirs[] = "<$input";
if ($output !== null) $redirs[] = "$gt$output";
if ($redirs) $redir = implode(" ", $redir);
} else {
switch ($redir) {
case "outonly":
case "noerr":
if ($output !== null) $redir = "$gt$output 2>/dev/null";
else $redir = "2>/dev/null";
break;
case "erronly":
case "noout":
if ($output !== null) $redir = "2$gt$output >/dev/null";
else $redir = "2>&1 >/dev/null";
break;
case "both":
case "err2out":
if ($output !== null) $redir = "$gt$output 2>&1";
else $redir = "2>&1";
break;
case "none":
case "null":
$redir = ">/dev/null 2>&1";
break;
}
}
if ($redir !== null) {
$cmd =& $this->cmds[$count - 1];
if ($cmd instanceof ICmd) {
$cmd->addRedir($redir);
} else {
$cmd = "$cmd $redir";
}
}
}
abstract function getCmd(?string $sep=null, bool $exec=false): string;
function passthru(int &$retcode=null): bool {
passthru($this->getCmd(), $retcode);
return $retcode == 0;
}
function system(string &$output=null, int &$retcode=null): bool {
$lastLine = system($this->getCmd(), $retcode);
if ($lastLine !== false) $output = $lastLine;
return $retcode == 0;
}
function exec(array &$output=null, int &$retcode=null): bool {
exec($this->getCmd(), $output, $retcode);
return $retcode == 0;
}
function fork_exec(int &$retcode=null): bool {
$cmd = $this->getCmd(null, true);
sh::_fork_exec($cmd, $retcode);
return $retcode == 0;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace nulib\os\proc;
use nulib\A;
use nulib\os\sh;
/**
* Class AbstractCmdList: une séquence de commandes séparées par ;, && ou ||
*/
abstract class AbstractCmdList extends AbstractCmd {
protected ?string $sep;
function __construct(?string $sep, $cmd=null, ?string $input=null, ?string $output=null) {
parent::__construct();
$this->sep = $sep;
$this->add($cmd, $input, $output);
}
function addLiteral($cmd): self {
A::append_nn($this->cmds, $cmd);
return $this;
}
function add($cmd, ?string $input=null, ?string $output=null): self {
if ($cmd !== null) {
if (!($cmd instanceof ICmd)) {
sh::verifix_cmd($cmd, null, $input, $output);
}
$this->cmds[] = $cmd;
}
return $this;
}
function getCmd(?string $sep=null, bool $exec=false): string {
if ($sep === null) $sep = "\n";
$actualCmd = [];
A::append_nn($actualCmd, $this->getSources($sep));
A::append_nn($actualCmd, $this->getVars($sep));
$parts = [];
foreach ($this->cmds as $cmd) {
if ($cmd instanceof ICmd) {
$cmd = "(".$cmd->getCmd($sep).")";
}
$parts[] = $cmd;
}
if (count($parts) == 1 && $exec) $parts[0] = "exec $parts[0]";
$actualCmd[] = implode($this->sep ?? $sep, $parts);
return implode($sep, $actualCmd);
}
}

19
php/src/os/proc/Cmd.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace nulib\os\proc;
/**
* Class CmdList: une séquence de commandes séparées par ;
*
* Toutes les commandes sont exécutées et le code d'erreur est celui de la
* dernière commande exécutée
*/
class Cmd extends AbstractCmdList {
static function with($cmd=null): Cmd {
if ($cmd instanceof Cmd) return $cmd;
return new static($cmd);
}
function __construct($cmd=null, ?string $input=null, ?string $output=null) {
parent::__construct(null, $cmd, $input, $output);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace nulib\os\proc;
/**
* Class CmdAnd: une séquence de commandes séparées par &&
*
* l'exécution s'arrête à la première erreur
*/
class CmdAnd extends AbstractCmdList {
function __construct($cmd=null, ?string $input=null, ?string $output=null) {
parent::__construct(" && ", $cmd, $input, $output);
}
}

13
php/src/os/proc/CmdOr.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace nulib\os\proc;
/**
* Class CmdOr: une séquence de commandes séparées par ||
*
* l'exécution s'arrête au premier succès
*/
class CmdOr extends AbstractCmdList {
function __construct($cmd=null, ?string $input=null, ?string $output=null) {
parent::__construct(" || ", $cmd, $input, $output);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace nulib\os\proc;
use nulib\A;
use nulib\os\sh;
/**
* Class CmdPipe: une suite de commandes qui doivent s'exécuter avec les sorties
* des unes connectées aux entrées des autres
*/
class CmdPipe extends AbstractCmd {
private ?string $input;
private ?string $output;
function __construct(?array $cmds=null, ?string $input=null, ?string $output=null) {
parent::__construct();
if ($cmds !== null) {
foreach ($cmds as $command) {
$this->add($command);
}
}
$this->input = $input;
$this->output = $output;
}
function addLiteral($cmd): self {
A::append_nn($this->cmds, $cmd);
return $this;
}
function add($cmd): self {
if ($cmd !== null) {
if (!($cmd instanceof ICmd)) {
sh::verifix_cmd($cmd);
}
$this->cmds[] = $cmd;
}
return $this;
}
function setInput(?string $input=null): self {
$this->input = $input;
return $this;
}
function setOutput(?string $output=null): self {
$this->output = $output;
return $this;
}
function getCmd(?string $sep=null, bool $exec=false): string {
if ($sep === null) $sep = "\n";
$actualCmd = [];
A::append_nn($actualCmd, $this->getSources($sep));
A::append_nn($actualCmd, $this->getVars($sep));
$parts = [];
foreach ($this->cmds as $cmd) {
if ($cmd instanceof ICmd) {
$cmd = "(".$cmd->getCmd($sep).")";
}
$parts[] = $cmd;
}
$cmd = implode(" | ", $parts);
$input = $this->input;
$output = $this->output;
if ($input !== null || $output !== null) {
$parts = [];
if ($input !== null) $parts[] = "<".escapeshellarg($input);
$parts[] = $cmd;
if ($output !== null) $parts[] = ">".escapeshellarg($output);
$cmd = implode(" ", $parts);
}
$actualCmd[] = $cmd;
return implode($sep, $actualCmd);
}
}

82
php/src/os/proc/ICmd.php Normal file
View File

@ -0,0 +1,82 @@
<?php
namespace nulib\os\proc;
/**
* Interface ICmd: une abstraction d'une ou plusieurs commandes à lancer
*/
interface ICmd {
/**
* vérifier si cette commande a besoin que son entrée standard soit connectée
* à un flux.
*/
function isNeedsStdin(): bool;
/**
* vérifier si cette commande a besoin que sa sortie standard soit connectée
* à un terminal
*/
function isNeedsTty(): bool;
/**
* Ajouter le préfixe spécifié à la dernière commande de la liste
*
* si $prefix est un array, quoter puis assembler les éléments du tableau.
* sinon ce doit être une chaine de caractère et elle est prise telle quelle
*/
function addPrefix($prefix): void;
/**
* Ajouter des redirections à la dernière commande de la liste
*
* $redir spécifie le type de redirection demandée:
* - "default" | null: $output reçoit STDOUT et STDERR n'est pas redirigé
* - "outonly" | "noerr": $output ne reçoit que STDOUT et STDERR est perdu
* - "erronly" | "noout": $output ne reçoit que STDERR et STDOUT est perdu
* - "both" | "err2out": $output reçoit STDOUT et STDERR
* - sinon c'est une redirection spécifique, et la valeur est rajoutée telle
* quelle à la ligne de commande ($output est ignoré)
*
* $output est le nom d'un fichier qui reçoit les redirections, ou null pour
* la valeur par défaut. spécifier $append==true pour ajouter au fichier
* $output au lieu de l'écraser
*/
function addRedir(?string $redir, $output=null, bool $append=false, $input=null): void;
/** Obtenir le texte de la commande comme elle serait saisie dans un shell */
function getCmd(?string $sep=null): string;
/**
* Lancer la commande avec passthru() et retourner le code de retour dans la
* variable $retcode
*
* voici la différence entre passthru(), system() et exec()
* +----------------+-----------------+----------------+----------------+
* | Command | Displays Output | Can Get Output | Gets Exit Code |
* +----------------+-----------------+----------------+----------------+
* | passthru() | Yes (raw) | No | Yes |
* | system() | Yes (as text) | Last line only | Yes |
* | exec() | No | Yes (array) | Yes |
* +----------------+-----------------+----------------+----------------+
*
* @return bool true si la commande s'est lancée sans erreur, false sinon
*/
function passthru(int &$retcode=null): bool;
/**
* Comme {@link passthru()} mais lancer la commande spécifiée avec system().
* Cf la doc de {@link passthru()} pour les autres détails
*/
function system(string &$output=null, int &$retcode=null): bool;
/**
* Comme {@link passthru()} mais lancer la commande spécifiée avec exec().
* Cf la doc de {@link passthru()} pour les autres détails
*/
function exec(array &$output=null, int &$retcode=null): bool;
/**
* Lancer la commande dans un processus fils via un shell et attendre la fin
* de son exécution
*/
function fork_exec(int &$retcode=null): bool;
}

View File

@ -2,7 +2,7 @@
namespace nulib\os; namespace nulib\os;
use nulib\cl; use nulib\cl;
use RuntimeException; use nulib\StateException;
class sh { class sh {
static final function _quote(string $value): string { static final function _quote(string $value): string {
@ -144,15 +144,12 @@ class sh {
$pid = pcntl_fork(); $pid = pcntl_fork();
if ($pid == -1) { if ($pid == -1) {
// parent, impossible de forker // parent, impossible de forker
throw new RuntimeException("unable to fork"); throw new StateException("unable to fork");
} elseif ($pid) { } elseif ($pid) {
// parent, fork ok // parent, fork ok
pcntl_waitpid($pid, $status); pcntl_waitpid($pid, $status);
if (pcntl_wifexited($status)) { if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status);
$retcode = pcntl_wexitstatus($status); else $retcode = 127;
} else {
$retcode = 127;
}
return $retcode == 0; return $retcode == 0;
} }
// child, fork ok // child, fork ok

View File

@ -1,5 +1,9 @@
# TOOD # TOOD
* dans msg::action($m, function() {}), *bloquer* la marque pour empêcher d'aller
plus bas que prévu. comme ça s'il y a plusieurs success ou failure dans la
fonction, c'est affiché correctement.
* [ ] possibilité de paramétrer le nom du fichier destination pour faire une * [ ] possibilité de paramétrer le nom du fichier destination pour faire une
rotation des logs rotation des logs
* [ ] lors de la rotation, si l'ouverture du nouveau fichier échoue, continuer * [ ] lors de la rotation, si l'ouverture du nouveau fichier échoue, continuer

View File

@ -1,44 +1,39 @@
<?php <?php
namespace nulib\output; namespace nulib\output;
use nulib\cl;
use nulib\str; use nulib\str;
use nulib\ValueException;
/** /**
* Class _messenger: classe de base pour say, log et msg * Class _messenger: classe de base pour say, log et msg
*/ */
abstract class _messenger { abstract class _messenger {
/** @var IMessenger */ abstract static function is_setup(): bool;
protected static $say; abstract static function set_messenger(IMessenger $msg);
/** @var IMessenger */
protected static $log;
abstract static function get(): IMessenger; abstract static function get(): IMessenger;
static function set_messenger_class(string $msg_class, ?array $params=null) {
if (!is_subclass_of($msg_class, IMessenger::class)) {
throw ValueException::invalid_class($msg_class, IMessenger::class);
}
static::set_messenger(new $msg_class($params));
}
static function create_or_reset_params(?array $params=null, string $log_class=null, ?array $create_params=null) {
if (static::is_setup()) {
self::reset_params($params);
} else {
$params = cl::merge($params, $create_params);
self::set_messenger_class($log_class, $params);
}
}
/** obtenir une nouvelle instance, avec un nouveau paramétrage */ /** obtenir une nouvelle instance, avec un nouveau paramétrage */
static function new(?array $params=null): IMessenger { static function new(?array $params=null): IMessenger {
return static::get()->clone($params); return static::get()->clone($params);
} }
/** @var IMessenger */
protected static $msg;
/** @var IMessenger[] */
protected static $stack;
/** pousser une nouvelle instance avec un nouveau paramétrage sur la pile */
static function push(?array $params=null) {
self::$stack[] = static::get();
self::$msg = self::new($params);
}
/** dépiler la précédente instance */
static function pop(): IMessenger {
if (self::$stack) $msg = self::$msg = array_pop(self::$stack);
else $msg = self::$msg;
return $msg;
}
static final function __callStatic($name, $args) { static final function __callStatic($name, $args) {
$name = str::us2camel($name); $name = str::us2camel($name);
call_user_func_array([static::get(), $name], $args); call_user_func_array([static::get(), $name], $args);

View File

@ -0,0 +1,28 @@
<?php
namespace nulib\output;
use nulib\output\std\ProxyMessenger;
/**
* Class console: afficher un message sur la console
*
* Cette classe DOIT être initialisée avant d'être utilisée
*/
class console extends _messenger {
private static ?IMessenger $msg = null;
private static bool $setup = false;
static function is_setup(): bool {
return self::$setup;
}
static function set_messenger(IMessenger $msg) {
self::$msg = $msg;
self::$setup = true;
}
static function get(): IMessenger {
return self::$msg ??= new ProxyMessenger();
}
}

View File

@ -1,49 +1,28 @@
<?php <?php
namespace nulib\output; namespace nulib\output;
use nulib\cl;
use nulib\output\std\ProxyMessenger; use nulib\output\std\ProxyMessenger;
use nulib\ValueException;
/** /**
* Class log: inscrire un message dans les logs uniquement * Class log: inscrire un message dans les logs uniquement
* *
* Cette classe (ou la classe parallèle {@link msg} DOIT être initialisée avant * Cette classe DOIT être initialisée avant d'être utilisée
* d'être utilisée
*/ */
class log extends _messenger { class log extends _messenger {
static function set_messenger(IMessenger $log=null) { private static ?IMessenger $msg = null;
self::$log = $log;
// forcer la recréation de l'instance partagée $msg private static bool $setup = false;
self::$msg = null;
static function is_setup(): bool {
return self::$setup;
} }
static function set_messenger_class(string $log_class=null, ?array $params=null) { static function set_messenger(IMessenger $msg) {
if (!is_subclass_of($log_class, IMessenger::class)) { self::$msg = $msg;
throw ValueException::invalid_class($log_class, IMessenger::class); self::$setup = true;
}
self::set_messenger(new $log_class($params));
} }
static function get(): IMessenger { static function get(): IMessenger {
if (self::$msg === null) { return self::$msg ??= new ProxyMessenger();
$msg = self::$log;
if ($msg === null) $msg = new ProxyMessenger();
self::$msg = $msg;
}
return self::$msg;
}
static function have_log(): bool {
return self::$log !== null;
}
static function create_or_reset_params(?array $params=null, string $log_class=null, ?array $create_params=null) {
if (self::$log === null) {
$params = cl::merge($params, $create_params);
self::set_messenger_class($log_class, $params);
} else {
self::reset_params($params);
}
} }
} }

View File

@ -2,54 +2,75 @@
namespace nulib\output; namespace nulib\output;
use nulib\output\std\ProxyMessenger; use nulib\output\std\ProxyMessenger;
use nulib\ValueException; use nulib\php\func;
/** /**
* Class msg: inscrire un message dans les logs ET l'afficher sur la console * Class msg: inscrire un message dans les logs ET l'afficher sur la console
* *
* Cette classe DOIT être initialisée avec {@link set_messenger()} ou * Cette classe DOIT être initialisée avec {@link set_messenger()} ou
* {@link set_messenger_class()} avant d'être utilisée. Une fois initialisée, * {@link set_messenger_class()} avant d'être utilisée.
* les classes {@link say} et {@link log} sont utilisables aussi
*/ */
class msg extends _messenger { class msg extends _messenger {
static function set_messenger(?IMessenger $say, ?IMessenger $log=null) { private static ?IMessenger $msg = null;
if ($say !== null) self::$say = $say;
if ($log !== null) self::$log = $log; private static bool $setup = false;
if ($say !== null || $log !== null) {
// forcer la recréation de l'instance partagée $msg static function is_setup(): bool {
self::$msg = null; return self::$setup;
}
} }
static function set_messenger_class(?string $say_class, ?string $log_class=null) { static function set_messenger(IMessenger $msg) {
if ($say_class !== null) { self::$msg = $msg;
if (!is_subclass_of($say_class, IMessenger::class)) { self::$setup = true;
throw ValueException::invalid_class($say_class, IMessenger::class);
}
self::$say = new $say_class();
}
if ($log_class !== null) {
if (!is_subclass_of($log_class, IMessenger::class)) {
throw ValueException::invalid_class($log_class, IMessenger::class);
}
self::$log = new $log_class();
}
if ($say_class !== null || $log_class !== null) {
// forcer la recréation de l'instance partagée $msg
self::$msg = null;
}
} }
static function get(): IMessenger { static function get(): IMessenger {
if (self::$msg === null) { return self::$msg ??= new ProxyMessenger();
$log = self::$log;
$say = self::$say;
if ($log !== null && $say !== null) $msg = new ProxyMessenger($log, $say);
elseif ($log !== null) $msg = $log;
elseif ($say !== null) $msg = $say;
else $msg = new ProxyMessenger();
self::$msg = $msg;
} }
return self::$msg;
/**
* initialiser les instances say, console, log.
*/
static function init(array $msgs) {
$say = $msgs["say"] ?? null;
$console = $msgs["console"] ?? null;
$log = $msgs["log"] ?? null;
$msgs = [];
if ($log !== null && $log !== false) {
if ($log instanceof IMessenger) log::set_messenger($log);
elseif (is_string($log)) log::set_messenger_class($log);
elseif (is_array($log)) {
func::ensure_class($log, $args);
$log = func::cons($log, $args);
}
log::set_messenger($log);
$msgs[] = $log;
}
if ($console !== null && $console !== false) {
if ($console instanceof IMessenger) console::set_messenger($console);
elseif (is_string($console)) console::set_messenger_class($console);
elseif (is_array($console)) {
func::ensure_class($console, $args);
$console = func::cons($console, $args);
}
console::set_messenger($console);
$msgs[] = $console;
}
if ($say !== null && $say !== false) {
if ($say instanceof IMessenger) say::set_messenger($say);
elseif (is_string($say)) say::set_messenger_class($say);
elseif (is_array($say)) {
func::ensure_class($say, $args);
$say = func::cons($say, $args);
}
say::set_messenger($say);
$msgs[] = $say;
}
if ($say === null && $console !== null) {
say::set_messenger($console);
} elseif ($console === null && $say !== null) {
console::set_messenger($say);
}
self::set_messenger(new ProxyMessenger(...$msgs));
} }
} }

Some files were not shown because too many files have changed in this diff Show More