maj nur/sery
This commit is contained in:
parent
73432809ad
commit
d68cf2b052
183
php/src/A.php
183
php/src/A.php
|
@ -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)); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
|
@ -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 où 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,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
283
php/src/cl.php
283
php/src/cl.php
|
@ -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 */
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
namespace nulib\db\_private;
|
||||||
|
|
||||||
|
class _create {
|
||||||
|
const SCHEMA = [
|
||||||
|
"prefix" => "?string",
|
||||||
|
"table" => "string",
|
||||||
|
"schema" => "?array",
|
||||||
|
"cols" => "?array",
|
||||||
|
"suffix" => "?string",
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
namespace nulib\db\_private;
|
||||||
|
|
||||||
|
class _delete {
|
||||||
|
const SCHEMA = [
|
||||||
|
"prefix" => "?string",
|
||||||
|
"from" => "?string",
|
||||||
|
"where" => "?array",
|
||||||
|
"suffix" => "?string",
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
namespace nulib\db\_private;
|
||||||
|
|
||||||
|
class _generic {
|
||||||
|
const SCHEMA = [
|
||||||
|
];
|
||||||
|
}
|
|
@ -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",
|
||||||
|
];
|
||||||
|
}
|
|
@ -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",
|
||||||
|
];
|
||||||
|
}
|
|
@ -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",
|
||||||
|
];
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
namespace nulib\ext\spreadsheet;
|
||||||
|
|
||||||
|
use nulib\file\csv\TAbstractReader;
|
||||||
|
|
||||||
|
class SsReader extends SpoutReader {
|
||||||
|
use TAbstractReader;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
namespace nulib\file\csv;
|
||||||
|
|
||||||
|
use IteratorAggregate;
|
||||||
|
|
||||||
|
interface IReader extends IteratorAggregate {
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue