maj nur/sery

This commit is contained in:
Jephté Clain 2024-11-14 20:00:00 +04:00
parent 914de961eb
commit 9a48a2bee4
59 changed files with 3760 additions and 1813 deletions

View File

@ -213,4 +213,23 @@ class A {
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)); }
#############################################################################
static final function sort(?array &$array, int $flags=SORT_REGULAR, bool $assoc=false): void {
if ($array === null) return;
if ($assoc) asort($array, $flags);
else sort($array, $flags);
}
static final function ksort(?array &$array, int $flags=SORT_REGULAR): void {
if ($array === null) return;
ksort($array, $flags);
}
static final function usort(?array &$array, array $keys, bool $assoc=false): void {
if ($array === null) return;
if ($assoc) uasort($array, cl::compare($keys));
else usort($array, cl::compare($keys));
}
}

View File

@ -21,7 +21,7 @@ class ExitError extends Error {
/** @var ?string */
protected $userMessage;
function haveMessage(): bool {
function haveUserMessage(): bool {
return $this->userMessage !== null;
}

View File

@ -1,11 +1,15 @@
<?php
namespace nulib\app;
use nulib\A;
use nulib\app;
use nulib\cl;
use nulib\file\SharedFile;
use nulib\os\path;
use nulib\os\sh;
use nulib\output\msg;
use nulib\php\time\DateTime;
use nulib\php\time\Elapsed;
use nulib\str;
/**
@ -18,21 +22,21 @@ class RunFile {
const NAME = null;
function __construct(?string $name, string $file, ?string $logfile=null) {
function __construct(?string $name, string $file, ?string $outfile=null) {
$file = path::ensure_ext($file, self::RUN_EXT);
$this->name = $name ?? static::NAME;
$this->file = new SharedFile($file);
$this->logfile = $logfile;
$this->outfile = $outfile;
}
protected ?string $name;
protected SharedFile $file;
protected ?string $logfile;
protected ?string $outfile;
function getLogfile(): ?string {
return $this->logfile;
function getOutfile(): ?string {
return $this->outfile;
}
protected static function merge(array $data, array $merge): array {
@ -41,29 +45,23 @@ class RunFile {
], $merge);
}
protected function initData(bool $forStart=true): array {
if ($forStart) {
$pid = posix_getpid();
$dateStart = new DateTime();
} else {
$pid = $dateStart = null;
}
protected function initData(): array {
return [
"name" => $this->name,
"id" => bin2hex(random_bytes(16)),
"pg_pid" => null,
"pid" => $pid,
"pgid" => null,
"pid" => null,
"serial" => 0,
# lock
"locked" => false,
"date_lock" => null,
"date_release" => null,
# run
"logfile" => $this->logfile,
"date_start" => $dateStart,
"logfile" => $this->outfile,
"date_start" => null,
"date_stop" => null,
"exitcode" => null,
"is_done" => null,
"is_reaped" => null,
"is_ack_done" => null,
# action
"action" => null,
"action_date_start" => null,
@ -73,9 +71,19 @@ class RunFile {
];
}
function reset(bool $delete=false) {
$file = $this->file;
if ($delete) {
$file->close();
unlink($file->getFile());
} else {
$file->ftruncate();
}
}
function read(): array {
$data = $this->file->unserialize();
if (!is_array($data)) $data = $this->initData(false);
if (!is_array($data)) $data = $this->initData();
return $data;
}
@ -84,7 +92,7 @@ class RunFile {
$file->lockWrite();
$data = $file->unserialize(null, false, true);
if (!is_array($data)) {
$data = $this->initData(false);
$data = $this->initData();
$file->ftruncate();
$file->serialize($data, false, true);
}
@ -160,14 +168,38 @@ class RunFile {
# 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
* Préparer le démarrage de l'application. Cette méhode est appelée par un
* script externe qui doit préparer le démarrage du script
*
* - démarrer un groupe de process dont le process courant est le leader
*/
function wfPrepare(?int &$pgid=null): void {
$this->update(function (array $data) use (&$pgid) {
posix_setsid();
$pgid = posix_getpid();
return cl::merge($this->initData(), [
"pgid" => $pgid,
"pid" => null,
"date_start" => new DateTime(),
]);
});
}
/** indiquer que l'application démarre. */
function wfStart(): void {
$this->update(function (array $data) {
return cl::merge($this->initData(), [
"pg_pid" => $data["pg_pid"],
$pid = posix_getpid();
if ($data["pgid"] !== null) {
A::merge($data, [
"pid" => $pid,
]);
} else {
$data = cl::merge($this->initData(), [
"pid" => $pid,
"date_start" => new DateTime(),
]);
}
return $data;
});
}
@ -183,13 +215,12 @@ class RunFile {
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;
function _getCid(array $data=null): int {
if ($data["pgid"] !== null) return -$data["pgid"];
else return $data["pid"];
}
function _isRunning(array $data=null): bool {
if (!posix_kill($data["pid"], 0)) {
switch (posix_get_last_error()) {
case 1: #PCNTL_EPERM:
@ -208,10 +239,22 @@ class RunFile {
return true;
}
/**
* 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;
return $this->_isRunning($data);
}
/** indiquer que l'application s'arrête */
function wfStop(): void {
$this->update(function (array $data) {
return ["date_stop" => new DateTime()];
return [
"date_stop" => new DateTime(),
];
});
}
@ -228,88 +271,75 @@ class RunFile {
}
/** après l'arrêt de l'application, mettre à jour le code de retour */
function wfStopped(int $exitcode): void {
function wfReaped(int $exitcode): void {
$this->update(function (array $data) use ($exitcode) {
return [
"pg_pid" => null,
"pgid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $exitcode,
"is_reaped" => true,
];
});
}
/**
* 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"]) {
private static function kill(int $pid, int $signal, ?string &$reason=null): bool {
if (!posix_kill($pid, $signal)) {
switch (posix_get_last_error()) {
case PCNTL_ESRCH:
$reason = "process inexistant";
break;
case PCNTL_EPERM:
$reason = "process non accessible";
break;
case PCNTL_EINVAL:
$reason = "signal invalide";
break;
}
return false;
}
$done = true;
if ($updateDone) return ["is_done" => $done];
else return null;
});
return $done;
return true;
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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,
];
});
function wfKill(?string &$reason=null): bool {
$data = $this->read();
$pid = $this->_getCid($data);
$stopped = false;
$timeout = 10;
$delay = 300000;
while (--$timeout >= 0) {
if (!self::kill($pid, SIGTERM, $reason)) return false;
usleep($delay);
$delay = 1000000; // attendre 1 seconde à partir de la deuxième fois
if (!$this->_isRunning($data)) {
$stopped = true;
break;
}
/** 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);
if (!$stopped) {
$timeout = 3;
$delay = 300000;
while (--$timeout >= 0) {
if (!self::kill($pid, SIGKILL, $reason)) return false;
usleep($delay);
$delay = 1000000; // attendre 1 seconde à partir de la deuxième fois
if (!$this->_isRunning($data)) {
$stopped = true;
break;
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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(),
];
});
}
}
if ($stopped) {
sh::_waitpid($pid, $exitcode);
$this->wfReaped($exitcode);
}
return $stopped;
}
/**
* vérifier si on est dans le cas la tâche devrait tourner mais en réalité
* ce n'est pas le cas
*/
function tm_isUndead(?int $pid=null): bool {
function _isUndead(?int $pid=null): bool {
$data = $this->read();
if ($data["date_start"] === null) return false;
if ($data["date_stop"] !== null) return false;
@ -332,23 +362,135 @@ class RunFile {
return false;
}
function tm_isReapable(): bool {
$data = $this->read();
return $data["date_stop"] !== null && $data["exitcode"] === null;
/**
* 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_ack_done"]) {
return false;
}
$done = true;
if ($updateDone) return ["is_ack_done" => $done];
else return null;
});
return $done;
}
/** 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) {
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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 [
"pg_pid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $data["exitcode"] ?? $exitcode,
"action" => $title,
"action_date_start" => new DateTime(),
"action_max_step" => $maxSteps,
"action_current_step" => 0,
];
});
app::_dispatch_signals();
}
/** 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,
];
});
app::_dispatch_signals();
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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);
}
function getDesc(?array $data=null, bool $withAction=true): ?array {
$data ??= $this->read();
$desc = $data["name"];
$dateStart = $data["date_start"];
$action = $withAction? $data["action"]: null;
$dateStop = $data["date_stop"];
$exitcode = $data["exitcode"];
if ($action !== null) {
$date ??= $data["action_date_step"];
$date ??= $data["action_date_start"];
if ($date !== null) $action = "$date $action";
$action = "Etape en cours: $action";
$current = $data["action_current_step"];
$max = $data["action_max_step"];
if ($current !== null && $max !== null) {
$action .= " ($current / $max)";
} elseif ($current !== null) {
$action .= " ($current)";
}
}
if ($exitcode !== null) {
$result = ["Code de retour $exitcode"];
if ($data["is_reaped"]) $result[] = "reaped";
if ($data["is_ack_done"]) $result[] = "acknowledged";
$result = join(", ", $result);
} else {
$result = null;
}
if (!$this->wasStarted($data)) {
$type = "neutral";
$haveLog = false;
$exitcode = null;
$message = [
"status" => "$desc: pas encore démarré",
];
} elseif ($this->isRunning($data)) {
$sinceStart = Elapsed::format_since($dateStart);
$type = "info";
$haveLog = true;
$exitcode = null;
$message = [
"status" => "$desc: EN COURS pid $data[pid]",
"started" => "Démarrée depuis $dateStart ($sinceStart)",
"action" => $action,
];
} elseif ($this->isStopped($data)) {
$duration = "\nDurée ".Elapsed::format_delay($dateStart, $dateStop);
$sinceStop = Elapsed::format_since($dateStop);
$haveLog = true;
if ($exitcode === null) $type = "warning";
elseif ($exitcode === 0) $type = "success";
else $type = "danger";
$message = [
"status" => "$desc: TERMINEE$duration",
"stopped" => "Arrêtée $sinceStop le $dateStop",
"result" => $result,
];
} else {
$type = "warning";
$haveLog = true;
$exitcode = null;
$message = [
"status" => "$desc: ETAT INCONNU",
"started" => "Commencée le $dateStart",
"stopped" => $dateStop? "Arrêtée le $dateStop": null,
"exitcode" => $result !== null? "Code de retour $result": null,
];
}
return [
"type" => $type,
"have_log" => $haveLog,
"exitcode" => $exitcode,
"message" => array_filter($message),
];
}
}

39
php/src/app/args.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace nulib\app;
use nulib\A;
use nulib\str;
class args {
/**
* transformer une liste d'argument de la forme
* - ["myArg" => $value] devient ["--my-arg", "$value"]
* - ["myOpt" => true] devient ["--my-opt"]
* - ["myOpt" => false] est omis
* - les autres valeurs sont prises telles quelles
*/
static function from_array(?array $array): array {
$args = [];
if ($array === null) return $args;
$index = 0;
foreach ($array as $arg => $value) {
if ($value === false) continue;
if ($arg === $index) {
$index++;
} else {
$arg = str::us2camel($arg);
$arg = str::camel2us($arg, false, "-");
$arg = str_replace("_", "-", $arg);
$args[] = "--$arg";
if (is_array($value)) $value[] = "--";
elseif ($value === true) $value = null;
}
if (is_array($value)) {
A::merge($args, array_map("strval", $value));
} elseif ($value !== null) {
$args[] = "$value";
}
}
return $args;
}
}

View File

@ -0,0 +1,29 @@
<?php
# script à inclure pour implémenter .launcher.php
# les constantes suivantes doivent être définies AVANT de chager ce script:
# - NULIB_APP_app_params : paramètres du projet
use nulib\os\path;
use nulib\app;
if ($argc <= 1) die("invalid arguments");
app::init(NULIB_APP_app_params);
$app = $argv[1];
array_splice($argv, 0, 1); $argc--;
if (class_exists($app)) {
# la configuration est celle actuellement chargée
$app::run();
} elseif (is_executable($app)) {
# la configuration est passée par une variable d'environnement
app::params_putenv();
pcntl_exec($app, array_slice($argv, 1));
} else {
# la configuration est celle actuellement chargée
$name = preg_replace('/\.php$/', "", path::basename($app));
app::init([
"name" => $name,
]);
require $app;
}

View File

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

View File

@ -2,7 +2,7 @@
namespace nulib;
use ArrayAccess;
use nulib\php\func;
use nulib\php\nur_func;
use Traversable;
/**
@ -28,6 +28,20 @@ class cl {
return $array;
}
/**
* construire un tableau avec le résultat de $row[$key] pour chaque élément
* de $rows
*/
static function all_get($key, ?iterable $rows): array {
$array = [];
if ($rows !== null) {
foreach ($rows as $row) {
$array[] = self::get($row, $key);
}
}
return $array;
}
/**
* retourner la première valeur de $array ou $default si le tableau est null
* ou vide
@ -322,7 +336,10 @@ class cl {
static final function merge2(...$arrays): ?array {
$merged = null;
foreach ($arrays as $array) {
foreach (self::with($array) as $key => $value) {
$array = self::withn($array);
if ($array === null) continue;
$merged ??= [];
foreach ($array as $key => $value) {
$merged[$key] = $value;
}
}
@ -334,9 +351,9 @@ class cl {
static final function map(callable $callback, ?iterable $array): array {
$result = [];
if ($array !== null) {
$ctx = func::_prepare($callback);
$ctx = nur_func::_prepare($callback);
foreach ($array as $key => $value) {
$result[$key] = func::_call($ctx, [$value, $key]);
$result[$key] = nur_func::_call($ctx, [$value, $key]);
}
}
return $result;
@ -710,15 +727,12 @@ class cl {
#############################################################################
static final function sorted(?array $array, int $flags=SORT_REGULAR, bool $assoc=false): ?array {
if ($array === null) return null;
if ($assoc) asort($array, $flags);
else sort($array, $flags);
A::sort($array, $flags, $assoc);
return $array;
}
static final function ksorted(?array $array, int $flags=SORT_REGULAR): ?array {
if ($array === null) return null;
ksort($array, $flags);
A::ksort($array, $flags);
return $array;
}
@ -749,9 +763,7 @@ class cl {
}
static final function usorted(?array $array, array $keys, bool $assoc=false): ?array {
if ($array === null) return null;
if ($assoc) uasort($array, self::compare($keys));
else usort($array, self::compare($keys));
A::usort($array, $keys, $assoc);
return $array;
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace nulib\db;
use nulib\php\func;
use nulib\php\nur_func;
use nulib\ValueException;
use Traversable;
@ -87,7 +87,7 @@ class Capacitor implements ITransactor {
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
nur_func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;

View File

@ -27,7 +27,7 @@ class CapacitorChannel {
if ($name !== null) {
$name = strtolower($name);
if ($tableName === null) {
$tableName = str_replace("-", "_", $tableName) . "_channel";
$tableName = str_replace("-", "_", $name) . "_channel";
}
} else {
$name = static::class;

View File

@ -3,7 +3,7 @@ namespace nulib\db;
use nulib\cl;
use nulib\db\cache\cache;
use nulib\php\func;
use nulib\php\nur_func;
use nulib\ValueException;
use Traversable;
@ -154,10 +154,14 @@ EOT;
return $this->_getCreateSql($this->getChannel($channel));
}
protected function _afterCreate(CapacitorChannel $channel): void {
}
protected function _create(CapacitorChannel $channel): void {
$channel->ensureSetup();
if (!$channel->isCreated()) {
$this->db->exec($this->_createSql($channel));
$this->_afterCreate($channel);
$channel->setCreated();
}
}
@ -178,8 +182,12 @@ EOT;
$this->_ensureExists($this->getChannel($channel));
}
protected function _beforeReset(CapacitorChannel $channel): void {
}
/** supprimer le canal spécifié */
function _reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->_beforeReset($channel);
$this->db->exec([
"drop table if exists",
$channel->getTableName(),
@ -224,8 +232,8 @@ EOT;
$initFunc = [$channel, "getItemValues"];
$initArgs = $args;
func::ensure_func($initFunc, null, $initArgs);
$values = func::call($initFunc, $item, ...$initArgs);
nur_func::ensure_func($initFunc, null, $initArgs);
$values = nur_func::call($initFunc, $item, ...$initArgs);
if ($values === [false]) return 0;
$row = cl::merge(
@ -253,7 +261,7 @@ EOT;
$insert = true;
$initFunc = [$channel, "onCreate"];
$initArgs = $args;
func::ensure_func($initFunc, null, $initArgs);
nur_func::ensure_func($initFunc, null, $initArgs);
$values = $this->unserialize($channel, $row);
$pvalues = null;
} else {
@ -270,12 +278,12 @@ EOT;
}
$initFunc = [$channel, "onUpdate"];
$initArgs = $args;
func::ensure_func($initFunc, null, $initArgs);
nur_func::ensure_func($initFunc, null, $initArgs);
$values = $this->unserialize($channel, $row);
$pvalues = $this->unserialize($channel, $prow);
}
$updates = func::call($initFunc, $item, $values, $pvalues, ...$initArgs);
$updates = nur_func::call($initFunc, $item, $values, $pvalues, ...$initArgs);
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
@ -287,8 +295,8 @@ EOT;
}
if ($func !== null) {
func::ensure_func($func, $channel, $args);
$updates = func::call($func, $item, $values, $pvalues, ...$args);
nur_func::ensure_func($func, $channel, $args);
$updates = nur_func::call($func, $item, $values, $pvalues, ...$args);
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
@ -502,8 +510,8 @@ EOT;
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);
nur_func::ensure_func($func, $channel, $args);
$onEach = nur_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();
@ -520,7 +528,7 @@ EOT;
$all = $this->_allCached("each", $channel, $filter, $mergeQuery);
foreach ($all as $values) {
$rowIds = $this->getRowIds($channel, $values);
$updates = func::_call($onEach, [$values["item"], $values, ...$args]);
$updates = nur_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");
@ -571,8 +579,8 @@ EOT;
function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onDelete;
func::ensure_func($func, $channel, $args);
$onEach = func::_prepare($func);
nur_func::ensure_func($func, $channel, $args);
$onEach = nur_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();
@ -588,7 +596,7 @@ EOT;
$all = $this->_allCached("delete", $channel, $filter);
foreach ($all as $values) {
$rowIds = $this->getRowIds($channel, $values);
$delete = boolval(func::_call($onEach, [$values["item"], $values, ...$args]));
$delete = boolval(nur_func::_call($onEach, [$values["item"], $values, ...$args]));
if ($delete) {
$db->exec([
"delete",

View File

@ -0,0 +1,36 @@
<?php
namespace nulib\db\_private;
use DateTimeInterface;
use nulib\php\time\Date;
use nulib\php\time\DateTime;
use nulib\str;
trait Tbindings {
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;
}
}
}

View File

@ -22,8 +22,8 @@ trait Tinsert {
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## insert
self::consume('insert\s*', $tmpsql);
$sql[] = "insert";
self::consume('(insert(?:\s+or\s+(?:ignore|replace))?)\s*', $tmpsql, $ms);
$sql[] = $ms[1];
## into
self::consume('into\s*', $tmpsql);

View File

@ -0,0 +1,35 @@
<?php
namespace nulib\db\_private;
use nulib\php\time\Date;
use nulib\php\time\DateTime;
trait Tvalues {
/**
* 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);
}
}

View File

@ -3,14 +3,17 @@ namespace nulib\db\pdo;
use Generator;
use nulib\cl;
use nulib\db\_private\Tvalues;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\php\func;
use nulib\php\nur_func;
use nulib\php\time\Date;
use nulib\php\time\DateTime;
use nulib\ValueException;
class Pdo implements IDatabase {
use Tvalues;
static function with($pdo, ?array $params=null): self {
if ($pdo instanceof static) {
return $pdo;
@ -118,8 +121,8 @@ class Pdo implements IDatabase {
$dbconn = $this->dbconn;
$options = $this->options;
if (is_callable($options)) {
func::ensure_func($options, $this, $args);
$options = func::call($options, ...$args);
nur_func::ensure_func($options, $this, $args);
$options = nur_func::call($options, ...$args);
}
$this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options);
_config::with($this->config)->configure($this);
@ -190,7 +193,7 @@ class Pdo implements IDatabase {
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
nur_func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
@ -219,34 +222,6 @@ class Pdo implements IDatabase {
}
}
/**
* 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);

View File

@ -1,7 +1,7 @@
<?php
namespace nulib\db\pdo;
use nulib\php\func;
use nulib\php\nur_func;
class _config {
static function with($configs): self {
@ -25,11 +25,11 @@ class _config {
function configure(Pdo $pdo): void {
foreach ($this->configs as $key => $config) {
if (is_string($config) && !func::is_method($config)) {
if (is_string($config) && !nur_func::is_method($config)) {
$pdo->exec($config);
} else {
func::ensure_func($config, $this, $args);
func::call($config, $pdo, $key, ...$args);
nur_func::ensure_func($config, $this, $args);
nur_func::call($config, $pdo, $key, ...$args);
}
}
}

View File

@ -1,14 +1,13 @@
<?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\db\_private\Tbindings;
use nulib\ValueException;
class _query_base extends _base {
use Tbindings;
protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
@ -55,33 +54,6 @@ class _query_base extends _base {
}
}
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 {

View File

@ -3,9 +3,10 @@ namespace nulib\db\sqlite;
use Generator;
use nulib\cl;
use nulib\db\_private\Tvalues;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\php\func;
use nulib\php\nur_func;
use nulib\ValueException;
use SQLite3;
use SQLite3Result;
@ -15,6 +16,8 @@ use SQLite3Stmt;
* Class Sqlite: frontend vers une base de données sqlite3
*/
class Sqlite implements IDatabase {
use Tvalues;
static function with($sqlite, ?array $params=null): self {
if ($sqlite instanceof static) {
return $sqlite;
@ -234,7 +237,7 @@ class Sqlite implements IDatabase {
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
nur_func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
@ -278,7 +281,8 @@ class Sqlite implements IDatabase {
try {
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row === false) return null;
elseif ($entireRow) return $row;
$this->verifixRow($row);
if ($entireRow) return $row;
else return cl::first($row);
} finally {
$result->finalize();
@ -299,6 +303,7 @@ class Sqlite implements IDatabase {
if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys);
try {
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
$this->verifixRow($row);
if ($primaryKeys !== null) {
$key = implode("-", cl::select($row, $primaryKeys));
yield $key => $row;

View File

@ -28,14 +28,67 @@ class SqliteStorage extends CapacitorStorage {
return self::format_sql($channel, $query->getSql());
}
function _exists(CapacitorChannel $channel): bool {
$tableName = $this->db->get([
"select name from sqlite_schema",
"where" => [
"name" => $channel->getTableName(),
function tableExists(string $tableName): bool {
$name = $this->db->get([
# depuis la version 3.33.0 le nom officiel de la table est sqlite_schema,
# mais le nom sqlite_master est toujours valable pour le moment
"select name from sqlite_master ",
"where" => ["name" => $tableName],
]);
return $name !== null;
}
function channelExists(string $name): bool {
$name = $this->db->get([
"select name from _channels",
"where" => ["name" => $name],
]);
return $name !== null;
}
protected function _afterCreate(CapacitorChannel $channel): void {
$db = $this->db;
if (!$this->tableExists("_channels")) {
# ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture
$db->exec([
"create table if not exists",
"table" => "_channels",
"cols" => [
"name" => "varchar primary key",
"table_name" => "varchar",
"class" => "varchar",
],
]);
return $tableName !== null;
}
if (!$this->channelExists($channel->getName())) {
# ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture
$db->exec([
"insert",
"into" => "_channels",
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class" => get_class($channel),
],
"suffix" => "on conflict do nothing",
]);
}
}
protected function _beforeReset(CapacitorChannel $channel): void {
$this->db->exec([
"delete",
"from" => "_channels",
"where" => [
"name" => $channel->getName(),
],
]);
}
function _exists(CapacitorChannel $channel): bool {
return $this->tableExists($channel->getTableName());
}
function close(): void {

View File

@ -1,7 +1,7 @@
<?php
namespace nulib\db\sqlite;
use nulib\php\func;
use nulib\php\nur_func;
class _config {
static function with($configs): self {
@ -25,11 +25,11 @@ class _config {
function configure(Sqlite $sqlite): void {
foreach ($this->configs as $key => $config) {
if (is_string($config) && !func::is_method($config)) {
if (is_string($config) && !nur_func::is_method($config)) {
$sqlite->exec($config);
} else {
func::ensure_func($config, $this, $args);
func::call($config, $sqlite, $key, ...$args);
nur_func::ensure_func($config, $this, $args);
nur_func::call($config, $sqlite, $key, ...$args);
}
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace nulib\db\sqlite;
use nulib\php\func;
use nulib\php\nur_func;
class _migration {
static function with($migrations): self {
@ -40,11 +40,11 @@ class _migration {
"value" => $migration,
"done" => 0,
]);
if (is_string($migration) && !func::is_method($migration)) {
if (is_string($migration) && !nur_func::is_method($migration)) {
$sqlite->exec($migration);
} else {
func::ensure_func($migration, $this, $args);
func::call($migration, $sqlite, $key, ...$args);
nur_func::ensure_func($migration, $this, $args);
nur_func::call($migration, $sqlite, $key, ...$args);
}
$sqlite->exec("update _migration set done = 1 where key = :key", [
"key" => $key,

View File

@ -2,11 +2,14 @@
namespace nulib\db\sqlite;
use nulib\db\_private\_base;
use nulib\db\_private\Tbindings;
use nulib\ValueException;
use SQLite3;
use SQLite3Stmt;
class _query_base extends _base {
use Tbindings;
protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
@ -42,6 +45,7 @@ class _query_base extends _base {
$close = true;
try {
foreach ($this->bindings as $param => $value) {
$this->verifixBindings($value);
SqliteException::check($db, $stmt->bindValue($param, $value));
}
$close = false;

67
php/src/ext/json.php Normal file
View File

@ -0,0 +1,67 @@
<?php
namespace nulib\ext;
use Exception;
use nulib\ext\json\JsonException;
use nulib\file;
use nulib\os\IOException;
/**
* Class json: gestion de données json
*/
class json {
static function decode(string $json, int $flags=0) {
return json_decode($json, true, 512, $flags);
}
static function encode($data, int $flags=0): string {
$flags |= JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE;
return json_encode($data, $flags);
}
static function check(string $json, &$data=null): bool {
try {
$data = self::decode($json, JSON_THROW_ON_ERROR);
return true;
} catch (Exception $e) {
return false;
}
}
/**
* @throws IOException si une erreur de lecture s'est produite
*/
static final function load($input): array {
$contents = file::reader($input)->getContents();
return JsonException::ensure_json_value(self::decode($contents));
}
/** obtenir la valeur JSON correspondant au corps de la requête POST */
static final function post_data() {
$content = file_get_contents("php://input");
return JsonException::ensure_json_value(self::decode($content));
}
/** envoyer $data au format JSON */
static final function send($data, bool $exit=true): void {
header("Content-Type: application/json");
print self::encode($data);
if ($exit) exit;
}
const INDENT_TABS = "\t";
static final function with($data, ?string $indent=null): string {
$json = self::encode($data, JSON_PRETTY_PRINT);
if ($indent !== null) {
$json = preg_replace_callback('/^(?: {4})+/m', function (array $ms) use ($indent) {
return str_repeat($indent, strlen($ms[0]) / 4);
}, $json);
}
return $json;
}
static final function dump($data, $output=null): void {
file::writer($output)->putContents(self::with($data));
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\php\json;
namespace nulib\ext\json;
use RuntimeException;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,13 @@ use nulib\file\TmpfileWriter;
* Class file: outils pour gérer les fichiers
*/
class file {
static function fix_dash($file) {
if ($file === "-") $file = null;
return $file;
}
static function reader($input, ?callable $func=null): FileReader {
$file = new FileReader($input);
$file = new FileReader(self::fix_dash($input));
if ($func !== null) {
try {
$func($file);
@ -25,7 +30,7 @@ class file {
}
static function writer($output, ?string $mode=null, ?callable $func=null): FileWriter {
$file = new FileWriter($output, $mode);
$file = new FileWriter(self::fix_dash($output), $mode);
if ($func !== null) {
try {
$func($file);

View File

@ -5,7 +5,7 @@ use DateTimeInterface;
use nulib\cl;
use nulib\file\TempStream;
use nulib\os\path;
use nulib\php\func;
use nulib\php\nur_func;
use nulib\php\time\DateTime;
use nulib\web\http;
@ -16,6 +16,12 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
/** @var ?array liste des colonnes à écrire */
const HEADERS = null;
/**
* @var bool faut-il écrire les en-têtes, soit celles qui sont spécifiées,
* soit celles qui sont calculées à partir des clés de la première ligne
*/
const USE_HEADERS = true;
/** @var ?string nom du fichier téléchargé */
const OUTPUT = null;
@ -23,14 +29,15 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
if ($output !== null) $params["output"] = $output;
$this->schema = $params["schema"] ?? static::SCHEMA;
$this->headers = $params["headers"] ?? static::HEADERS;
$this->useHeaders = $params["use_headers"] ?? static::USE_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);
nur_func::ensure_func($cookFunc, $this, $cookArgs);
$cookCtx = nur_func::_prepare($cookFunc);
}
$this->cookCtx = $cookCtx;
$this->cookArgs = $cookArgs;
@ -44,6 +51,8 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
protected ?array $headers;
protected bool $useHeaders;
protected ?iterable $rows;
protected ?string $output;
@ -53,7 +62,7 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
protected ?array $cookArgs;
protected function ensureHeaders(?array $row=null): void {
if ($this->headers !== null) return;
if ($this->headers !== null || !$this->useHeaders) return;
if ($this->schema === null) $headers = null;
else $headers = array_keys($this->schema);
if ($headers === null && $row !== null) $headers = array_keys($row);
@ -66,16 +75,18 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
function writeHeaders(?array $headers=null): void {
if ($this->wroteHeaders) return;
if ($this->useHeaders) {
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);
$row = nur_func::_call($this->cookCtx, $args);
}
if ($row !== null) {
foreach ($row as &$value) {

View File

@ -10,6 +10,9 @@ abstract class AbstractReader implements IReader {
const HEADERS = null;
/** @var bool faut-il utiliser les en-têtes pour retourner des tableaux associatifs? */
const USE_HEADERS = true;
/** @var ?string nom du fichier depuis lequel lire */
const INPUT = null;
@ -17,18 +20,28 @@ abstract class AbstractReader implements IReader {
const TRIM = true;
/** @var bool faut-il considérer les chaines vides comme null? */
const PARSE_EMPTY_AS_NULL = true;
const EMPTY_AS_NULL = true;
/**
* @var bool ne pas essayer de deviner le format des données: les laisser au
* format chaine.
*/
const PARSE_NOT = false;
/**
* @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
* si false, ne forcer le type numérique que si la chaine ne commence pas par
* zéro i.e "06" est une chaine, alors "63" est un nombre
*
* ce paramètre est ignoré si PARSE_NOT == true
*/
const PARSE_NUMERIC = false;
/**
* @var bool faut-il forcer le type {@link Date} ou {@link DateTime} pour une
* chaine au bon format?
*
* ce paramètre est ignoré si PARSE_NOT == true
*/
const PARSE_DATE = true;
@ -37,9 +50,11 @@ abstract class AbstractReader implements IReader {
#
$this->schema = $params["schema"] ?? static::SCHEMA;
$this->headers = $params["headers"] ?? static::HEADERS;
$this->useHeaders = $params["use_headers"] ?? static::USE_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->emptyAsNull = boolval($params["empty_as_null"] ?? static::EMPTY_AS_NULL);
$this->parseNot = boolval($params["parse_not"] ?? static::PARSE_NOT);
$this->parseNumeric = boolval($params["parse_numeric"] ?? static::PARSE_NUMERIC);
$this->parseDate = boolval($params["parse_date"] ?? static::PARSE_DATE);
}
@ -48,11 +63,15 @@ abstract class AbstractReader implements IReader {
protected ?array $headers;
protected bool $useHeaders;
protected $input;
protected bool $trim;
protected bool $parseEmptyAsNull;
protected bool $emptyAsNull;
protected bool $parseNot;
protected bool $parseNumeric;
@ -60,7 +79,8 @@ abstract class AbstractReader implements IReader {
protected int $isrc = 0, $idest = 0;
protected function cook(array &$row): bool {
protected function cookRow(array &$row): bool {
if (!$this->useHeaders) return true;
if ($this->isrc == 0) {
# ligne d'en-tête
$headers = $this->headers;
@ -81,11 +101,11 @@ abstract class AbstractReader implements IReader {
if ($this->trim && is_string($col)) {
$col = trim($col);
}
if ($this->parseEmptyAsNull && $col === "") {
if ($this->emptyAsNull && $col === "") {
# valeur vide --> null
$col = null;
}
if (!is_string($col)) return;
if (!is_string($col) || $this->parseNot) return;
if ($this->parseDate) {
if (DateTime::isa_datetime($col, true)) {
# datetime

View File

@ -1,6 +1,7 @@
<?php
namespace nulib\file\csv;
use nulib\file;
use nulib\file\FileReader;
class CsvReader extends AbstractReader {
@ -17,7 +18,7 @@ class CsvReader extends AbstractReader {
protected ?string $inputEncoding;
function getIterator() {
$reader = new FileReader($this->input);
$reader = new FileReader(file::fix_dash($this->input));
$inputEncoding = $this->inputEncoding;
if ($inputEncoding !== null) {
$reader->appendFilter("convert.iconv.$inputEncoding.utf-8");
@ -27,7 +28,7 @@ class CsvReader extends AbstractReader {
foreach ($row as &$col) {
$this->verifixCol($col);
}; unset($col);
if ($this->cook($row)) {
if ($this->cookRow($row)) {
yield $row;
$this->idest++;
}

View File

@ -1,7 +1,7 @@
<?php
namespace nulib\file\csv;
interface IBuilder {
interface IBuilder extends \nulib\file\IReader {
function writeHeaders(?array $headers=null): void;
function write(?array $row): void;

View File

@ -37,17 +37,15 @@ trait TAbstractBuilder {
$output = $params["output"] ?? null;
$ssType = null;
if (is_string($output)) {
switch (path::ext($output)) {
case ".csv":
$ext = path::ext($output);
if ($output === "-" || $ext === ".csv") {
$class = CsvBuilder::class;
break;
case ".ods":
} elseif ($ext === ".ods") {
$ssType = "ods";
break;
case ".xlsx":
default:
} elseif ($ext === ".xlsx") {
$ssType = "xlsx";
} else {
$ssType = "xlsx";
break;
}
}
$params["ss_type"] = $ssType;

View File

@ -36,17 +36,15 @@ trait TAbstractReader {
$input = $params["input"] ?? null;
$ssType = null;
if (is_string($input)) {
switch (path::ext($input)) {
case ".csv":
$ext = path::ext($input);
if ($input === "-" || $ext === ".csv") {
$class = CsvReader::class;
break;
case ".ods":
} elseif ($ext === ".ods") {
$ssType = "ods";
break;
case ".xlsx":
default:
} elseif ($ext === ".xlsx") {
$ssType = "xlsx";
} else {
$ssType = "xlsx";
break;
}
}
$params["ss_type"] = $ssType;

View File

@ -193,9 +193,9 @@ abstract class AbstractCmd implements ICmd {
return $retcode == 0;
}
function fork_exec(int &$retcode=null): bool {
function fork_exec(?int &$retcode=null, bool $wait=true): bool {
$cmd = $this->getCmd(null, true);
sh::_fork_exec($cmd, $retcode);
sh::_fork_exec($cmd, $retcode, $wait);
return $retcode == 0;
}
}

View File

@ -1,7 +1,9 @@
<?php
namespace nulib\os;
use nulib\app;
use nulib\cl;
use nulib\ExitError;
use nulib\StateException;
class sh {
@ -49,7 +51,7 @@ class sh {
return implode(" ", $parts);
}
private static final function add_redir(string &$cmd, ?string $redir, ?string $input, ?string $output): void {
private static function add_redir(string &$cmd, ?string $redir, ?string $input, ?string $output): void {
if ($redir !== null) {
switch ($redir) {
case "outonly":
@ -134,27 +136,34 @@ class sh {
return $retcode == 0;
}
static function _waitpid(int $pid, ?int &$retcode=null): bool {
pcntl_waitpid($pid, $status);
if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status);
elseif (pcntl_wifsignaled($status)) $retcode = -pcntl_wtermsig($status);
else $retcode = app::EC_FORK_CHILD;
return $retcode == 0;
}
/**
* Lancer la commande $cmd dans un processus fils via un shell et attendre la
* fin de son exécution.
*
* $cmd doit déjà être formaté comme il convient
*/
static final function _fork_exec(string $cmd, int &$retcode=null): bool {
static final function _fork_exec(string $cmd, ?int &$retcode=null, bool $wait=true): bool {
$pid = pcntl_fork();
if ($pid == -1) {
// parent, impossible de forker
throw new StateException("unable to fork");
throw new ExitError(app::EC_FORK_PARENT, "unable to fork");
} elseif ($pid) {
// parent, fork ok
pcntl_waitpid($pid, $status);
if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status);
else $retcode = 127;
return $retcode == 0;
if ($wait) return self::_waitpid($pid, $retcode);
$retcode = null;
return true;
}
// child, fork ok
pcntl_exec("/bin/sh", ["-c", $cmd]);
return false;
throw StateException::unexpected_state();
}
/**
@ -304,4 +313,51 @@ class sh {
if (!is_link($f) || !is_link($g)) return false;
return @readlink($f) === @readlink($g);
}
#############################################################################
static function ls_all(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
$all = scandir($dir, $sorting_order);
if ($all === false) return [];
return array_values(array_filter($all,
function ($file) use ($pattern) {
if ($file === "." || $file === "..") return false;
return $pattern === null || fnmatch($pattern, $file);
}
));
}
static function ls_dirs(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
return array_values(array_filter(self::ls_all($dir, $pattern, $sorting_order),
function ($file) use ($dir) {
return path::is_dir(path::join($dir, $file));
}
));
}
static function ls_files(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
return array_values(array_filter(self::ls_all($dir, $pattern, $sorting_order),
function ($file) use ($dir) {
return path::is_file(path::join($dir, $file));
}
));
}
static function ls_pall(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
return array_map(function(string $name) use ($dir) {
return path::join($dir, $name);
}, self::ls_all($dir, $pattern, $sorting_order));
}
static function ls_pdirs(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
return array_map(function(string $name) use ($dir) {
return path::join($dir, $name);
}, self::ls_dirs($dir, $pattern, $sorting_order));
}
static function ls_pfiles(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
return array_map(function(string $name) use ($dir) {
return path::join($dir, $name);
}, self::ls_files($dir, $pattern, $sorting_order));
}
}

View File

@ -2,10 +2,10 @@
namespace nulib\output;
use nulib\output\std\ProxyMessenger;
use nulib\php\func;
use nulib\php\nur_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 à l'utilisateur
*
* Cette classe DOIT être initialisée avec {@link set_messenger()} ou
* {@link set_messenger_class()} avant d'être utilisée.
@ -40,8 +40,8 @@ class msg extends _messenger {
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);
nur_func::ensure_class($log, $args);
$log = nur_func::cons($log, $args);
}
log::set_messenger($log);
$msgs[] = $log;
@ -50,8 +50,8 @@ class msg extends _messenger {
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);
nur_func::ensure_class($console, $args);
$console = nur_func::cons($console, $args);
}
console::set_messenger($console);
$msgs[] = $console;
@ -60,8 +60,8 @@ class msg extends _messenger {
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);
nur_func::ensure_class($say, $args);
$say = nur_func::cons($say, $args);
}
say::set_messenger($say);
$msgs[] = $say;

View File

@ -241,6 +241,7 @@ class StdMessenger implements _IMessenger {
int $indentLevel, StdOutput $out): void {
$prefixes = self::GENERIC_PREFIXES[$level][$type];
if ($prefixes[0]) $out->print();
$content = cl::with($content);
if ($out->isColor()) {
$before = $prefixes[2];
$prefix = $prefixes[3];
@ -249,7 +250,7 @@ class StdMessenger implements _IMessenger {
$suffix2 = $suffix !== null? " $suffix": null;
$after = $prefixes[5];
$lines = $out->getLines(false, $content);
$lines = $out->getLines(false, ...$content);
$maxlen = 0;
foreach ($lines as &$content) {
$line = $out->filterColors($content);
@ -274,7 +275,7 @@ class StdMessenger implements _IMessenger {
$prefix = $prefixes[1];
if ($prefix !== null) $prefix .= " ";
$prefix2 = str_repeat(" ", mb_strlen($prefix));
$lines = $out->getLines(false, $content);
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content);
@ -360,6 +361,7 @@ class StdMessenger implements _IMessenger {
string $type, $content,
int $indentLevel, StdOutput $out): void {
$prefixes = self::GENERIC_PREFIXES[$level][$type];
$content = cl::with($content);
if ($out->isColor()) {
$prefix = $prefixes[1];
$prefix2 = null;
@ -369,7 +371,7 @@ class StdMessenger implements _IMessenger {
$prefix2 = str_repeat(" ", mb_strlen($prefix2));
}
$suffix = $prefixes[2];
$lines = $out->getLines(false, $content);
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content, $suffix);
@ -379,7 +381,7 @@ class StdMessenger implements _IMessenger {
$prefix = $prefixes[0];
if ($prefix !== null) $prefix .= " ";
$prefix2 = str_repeat(" ", mb_strlen($prefix));
$lines = $out->getLines(false, $content);
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content);

View File

@ -3,7 +3,7 @@ namespace nulib\php\content;
use Closure;
use nulib\cl;
use nulib\php\func;
use nulib\php\nur_func;
/**
* Class c: classe outil pour gérer du contenu
@ -26,7 +26,7 @@ class c {
}
const nq = [self::class, "nq"];
private static final function add_static_content(array &$dest, iterable $values, $key, bool $seq): void {
private static function add_static_content(array &$dest, iterable $values, $key, bool $seq): void {
$sindex = 0;
foreach ($values as $skey => $svalue) {
if ($skey === $sindex) {
@ -62,8 +62,8 @@ class c {
# contenu dynamique: le contenu est la valeur de retour de la fonction
# ce contenu est rajouté à la suite après avoir été quoté avec self::q()
$func = $value;
func::ensure_func($func, $object_or_class, $args);
$values = self::q(func::call($func, ...$args));
nur_func::ensure_func($func, $object_or_class, $args);
$values = self::q(nur_func::call($func, ...$args));
self::add_static_content($dest, $values, $key, $seq);
continue;
}
@ -83,15 +83,15 @@ class c {
$arg = self::resolve($arg, $object_or_class, false);
if (!$array) $arg = $arg[0];
}; unset($arg);
if (func::is_static($func)) {
func::ensure_func($func, $object_or_class, $args);
$value = func::call($func, ...$args);
} elseif (func::is_class($func)) {
func::fix_class_args($func, $args);
$value = func::cons($func, ...$args);
if (nur_func::is_static($func)) {
nur_func::ensure_func($func, $object_or_class, $args);
$value = nur_func::call($func, ...$args);
} elseif (nur_func::is_class($func)) {
nur_func::fix_class_args($func, $args);
$value = nur_func::cons($func, ...$args);
} else {
func::ensure_func($func, $object_or_class, $args);
$value = func::call($func, ...$args);
nur_func::ensure_func($func, $object_or_class, $args);
$value = nur_func::call($func, ...$args);
}
}
}
@ -102,14 +102,14 @@ class c {
}
const resolve = [self::class, "resolve"];
private static final function wend(?string $value): bool {
private static function wend(?string $value): bool {
return $value !== null && preg_match('/(\w|\w\.)$/', $value);
}
private static final function startw(?string $value): bool {
private static function startw(?string $value): bool {
return $value !== null && preg_match('/^\w/', $value);
}
private static final function to_values($content, ?array &$values=null): void {
private static function to_values($content, ?array &$values=null): void {
$pvalue = cl::last($values);
$wend = self::wend($pvalue);
foreach ($content as $value) {

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ class mprop {
} catch (ReflectionException $e) {
return oprop::get($object, $property, $default);
}
return func::call([$object, $m], $default);
return nur_func::call([$object, $m], $default);
}
/** spécifier la valeur d'une propriété */
@ -53,14 +53,14 @@ class mprop {
return self::_set($c, $object, $property, $value, $method);
}
private static final function _set(ReflectionClass $c, object $object, string $property, $value, ?string $method) {
private static function _set(ReflectionClass $c, object $object, string $property, $value, ?string $method) {
if ($method === null) $method = self::get_setter_name($property);
try {
$m = $c->getMethod($method);
} catch (ReflectionException $e) {
return oprop::_set($c, $object, $property, $value);
}
func::call([$object, $m], $value);
nur_func::call([$object, $m], $value);
return $value;
}

453
php/src/php/nur_func.php Normal file
View File

@ -0,0 +1,453 @@
<?php
namespace nulib\php;
use Closure;
use nulib\cl;
use nulib\ref\php\ref_func;
use nulib\ValueException;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
/**
* Class func: outils pour appeler des fonctions et méthodes dynamiquement
*/
class nur_func {
/**
* tester si $func est une chaine de la forme "XXX::method" XXX est une
* chaine quelconque éventuellement vide, ou un tableau de la forme ["method"]
* ou [anything, "method", ...]
*
* Avec la forme tableau, "method" ne doit pas contenir le caractère '\', pour
* pouvoir utiliser conjointement {@link is_class()}
*/
static final function is_static($func, bool $allowClass=false): bool {
if (is_string($func)) {
$pos = strpos($func, "::");
if ($pos === false) return false;
return $pos + 2 < strlen($func);
} elseif (is_array($func) && array_key_exists(0, $func)) {
$count = count($func);
if ($count == 1) {
if (!is_string($func[0]) || strlen($func[0]) == 0) return false;
if (strpos($func[0], "\\") !== false) return false;
return true;
} elseif ($count > 1) {
if (!array_key_exists(1, $func)) return false;
if (!is_string($func[1]) || strlen($func[1]) == 0) return false;
if (strpos($func[1], "\\") !== false) return false;
return true;
}
}
return false;
}
/**
* si $func est une chaine de la forme "::method" alors la remplacer par la
* chaine "$class::method"
*
* si $func est un tableau de la forme ["method"] ou [null, "method"], alors
* le remplacer par [$class, "method"]
*
* on assume que {@link is_static()}($func) retourne true
*
* @return bool true si la correction a été faite
*/
static final function fix_static(&$func, $class): bool {
if (is_object($class)) $class = get_class($class);
if (is_string($func) && substr($func, 0, 2) == "::") {
$func = "$class$func";
return true;
} elseif (is_array($func) && array_key_exists(0, $func)) {
$count = count($func);
if ($count == 1) {
$func = [$class, $func[0]];
return true;
} elseif ($count > 1 && $func[0] === null) {
$func[0] = $class;
return true;
}
}
return false;
}
/** tester si $method est une chaine de la forme "->method" */
private static function isam($method): bool {
return is_string($method)
&& strlen($method) > 2
&& substr($method, 0, 2) == "->";
}
/**
* tester si $func est une chaine de la forme "->method" ou un tableau de la
* forme ["->method", ...] ou [anything, "->method", ...]
*/
static final function is_method($func): bool {
if (is_string($func)) {
return self::isam($func);
} elseif (is_array($func) && array_key_exists(0, $func)) {
if (self::isam($func[0])) {
# ["->method", ...]
return true;
}
if (array_key_exists(1, $func) && self::isam($func[1])) {
# [anything, "->method", ...]
return true;
}
}
return false;
}
/**
* si $func est une chaine de la forme "->method" alors la remplacer par le
* tableau [$object, "method"]
*
* si $func est un tableau de la forme ["->method"] ou [anything, "->method"],
* alors le remplacer par [$object, "method"]
*
* @return bool true si la correction a été faite
*/
static final function fix_method(&$func, $object): bool {
if (!is_object($object)) return false;
if (is_string($func)) {
if (self::isam($func)) {
$func = [$object, substr($func, 2)];
return true;
}
} elseif (is_array($func) && array_key_exists(0, $func)) {
if (self::isam($func[0])) $func = array_merge([null], $func);
if (count($func) > 1 && array_key_exists(1, $func) && self::isam($func[1])) {
$func[0] = $object;
$func[1] = substr($func[1], 2);
return true;
}
}
return false;
}
/**
* si $func est un tableau de plus de 2 éléments, alors déplacer les éléments
* supplémentaires au début de $args. par exemple:
* ~~~
* $func = ["class", "method", "arg1", "arg2"];
* $args = ["arg3"];
* func::fix_args($func, $args)
* # $func === ["class", "method"]
* # $args === ["arg1", "arg2", "arg3"]
* ~~~
*
* @return bool true si la correction a été faite
*/
static final function fix_args(&$func, ?array &$args): bool {
if ($args === null) $args = [];
if (is_array($func) && count($func) > 2) {
$prefix_args = array_slice($func, 2);
$func = array_slice($func, 0, 2);
$args = array_merge($prefix_args, $args);
return true;
}
return false;
}
/**
* s'assurer que $func est un appel de méthode ou d'une méthode statique;
* et renseigner le cas échéant les arguments. si $func ne fait pas mention
* de la classe ou de l'objet, le renseigner avec $class_or_object.
*
* @return bool true si c'est une fonction valide. il ne reste plus qu'à
* l'appeler avec {@link call()}
*/
static final function check_func(&$func, $class_or_object, &$args=null): bool {
if ($func instanceof Closure) return true;
if (self::is_method($func)) {
# méthode
self::fix_method($func, $class_or_object);
self::fix_args($func, $args);
return true;
} elseif (self::is_static($func)) {
# méthode statique
self::fix_static($func, $class_or_object);
self::fix_args($func, $args);
return true;
}
return false;
}
/**
* Comme {@link check_func()} mais lance une exception si la fonction est
* invalide
*
* @throws ValueException si $func n'est pas une fonction ou une méthode valide
*/
static final function ensure_func(&$func, $class_or_object, &$args=null): void {
if (!self::check_func($func, $class_or_object, $args)) {
throw ValueException::invalid_type($func, "callable");
}
}
static final function _prepare($func): array {
$object = null;
if (is_callable($func)) {
if (is_array($func)) {
$rf = new ReflectionMethod(...$func);
$object = $func[0];
if (is_string($object)) $object = null;
} elseif ($func instanceof Closure) {
$rf = new ReflectionFunction($func);
} elseif (is_string($func) && strpos($func, "::") === false) {
$rf = new ReflectionFunction($func);
} else {
$rf = new ReflectionMethod($func);
}
} elseif ($func instanceof ReflectionMethod) {
$rf = $func;
} elseif ($func instanceof ReflectionFunction) {
$rf = $func;
} elseif (is_array($func) && count($func) == 2 && isset($func[0]) && isset($func[1])
&& ($func[1] instanceof ReflectionMethod || $func[1] instanceof ReflectionFunction)) {
$object = $func[0];
if (is_string($object)) $object = null;
$rf = $func[1];
} elseif (is_string($func) && strpos($func, "::") === false) {
$rf = new ReflectionFunction($func);
} else {
throw ValueException::invalid_type($func, "callable");
}
$minArgs = $rf->getNumberOfRequiredParameters();
$maxArgs = $rf->getNumberOfParameters();
$variadic = $rf->isVariadic();
return [$rf instanceof ReflectionMethod, $object, $rf, $minArgs, $maxArgs, $variadic];
}
static final function _fill(array $context, array &$args): void {
$minArgs = $context[3];
$maxArgs = $context[4];
$variadic = $context[5];
if (!$variadic) $args = array_slice($args, 0, $maxArgs);
while (count($args) < $minArgs) $args[] = null;
}
static final function _call($context, array $args) {
self::_fill($context, $args);
$use_object = $context[0];
$object = $context[1];
$method = $context[2];
if ($use_object) {
if (count($args) === 0) return $method->invoke($object);
else return $method->invokeArgs($object, $args);
} else {
if (count($args) === 0) return $method->invoke();
else return $method->invokeArgs($args);
}
}
/**
* Appeler la fonction spécifiée avec les arguments spécifiés.
* Adapter $args en fonction du nombre réel d'arguments de $func
*
* @param callable|ReflectionFunction|ReflectionMethod $func
*/
static final function call($func, ...$args) {
return self::_call(self::_prepare($func), $args);
}
/** remplacer $value par $func($value, ...$args) */
static final function apply(&$value, $func, ...$args): void {
if ($func !== null) {
if ($args) $args = array_merge([$value], $args);
else $args = [$value];
$value = self::call($func, ...$args);
}
}
const MASK_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC;
const MASK_P = ReflectionMethod::IS_PUBLIC;
const METHOD_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC;
const METHOD_P = ReflectionMethod::IS_PUBLIC;
private static function matches(string $name, array $includes, array $excludes): bool {
if ($includes) {
$matches = false;
foreach ($includes as $include) {
if (substr($include, 0, 1) == "/") {
# expression régulière
if (preg_match($include, $name)) {
$matches = true;
break;
}
} else {
# tester la présence de la sous-chaine
if (strpos($name, $include) !== false) {
$matches = true;
break;
}
}
}
if (!$matches) return false;
}
foreach ($excludes as $exclude) {
if (substr($exclude, 0, 1) == "/") {
# expression régulière
if (preg_match($exclude, $name)) return false;
} else {
# tester la présence de la sous-chaine
if (strpos($name, $exclude) !== false) return false;
}
}
return true;
}
/** @var Schema */
private static $call_all_params_schema;
/**
* retourner la liste des méthodes de $class_or_object qui correspondent au
* filtre $options. le filtre doit respecter le schéme {@link CALL_ALL_PARAMS_SCHEMA}
*/
static function get_all($class_or_object, $params=null): array {
Schema::nv($paramsv, $params, null
, self::$call_all_params_schema, ref_func::CALL_ALL_PARAMS_SCHEMA);
if (is_callable($class_or_object, true) && is_array($class_or_object)) {
# callable sous forme de tableau
$class_or_object = $class_or_object[0];
}
if (is_string($class_or_object)) {
# lister les méthodes publiques statiques de la classe
$mask = self::MASK_PS;
$expected = self::METHOD_PS;
$c = new ReflectionClass($class_or_object);
} elseif (is_object($class_or_object)) {
# lister les méthodes publiques de la classe
$c = new ReflectionClass($class_or_object);
$mask = $params["static_only"]? self::MASK_PS: self::MASK_P;
$expected = $params["static_only"]? self::METHOD_PS: self::METHOD_P;
} else {
throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet");
}
$prefix = $params["prefix"]; $prefixlen = strlen($prefix);
$args = $params["args"];
$includes = $params["include"];
$excludes = $params["exclude"];
$methods = [];
foreach ($c->getMethods() as $m) {
if (($m->getModifiers() & $mask) != $expected) continue;
$name = $m->getName();
if (substr($name, 0, $prefixlen) != $prefix) continue;
if (!self::matches($name, $includes, $excludes)) continue;
$methods[] = cl::merge([$class_or_object, $name], $args);
}
return $methods;
}
/**
* Appeler toutes les méthodes publiques de $object_or_class et retourner un
* tableau [$method_name => $return_value] des valeurs de retour.
*/
static final function call_all($class_or_object, $params=null): array {
$methods = self::get_all($class_or_object, $params);
$values = [];
foreach ($methods as $method) {
self::fix_args($method, $args);
$values[$method[1]] = self::call($method, ...$args);
}
return $values;
}
/**
* tester si $func est une chaine de la forme "XXX" XXX est une classe
* valide, ou un tableau de la forme ["XXX", ...]
*
* NB: il est possible d'avoir {@link is_static()} et {@link is_class()}
* vraies pour la même valeur. s'il faut supporter les deux cas, appeler
* {@link is_static()} d'abord, mais dans ce cas, on ne supporte que les
* classes qui sont dans un package
*/
static final function is_class($class): bool {
if (is_string($class)) {
return class_exists($class);
} elseif (is_array($class) && array_key_exists(0, $class)) {
return class_exists($class[0]);
}
return false;
}
/**
* en assumant que {@link is_class()} est vrai, si $class est un tableau de
* plus de 1 éléments, alors déplacer les éléments supplémentaires au début de
* $args. par exemple:
* ~~~
* $class = ["class", "arg1", "arg2"];
* $args = ["arg3"];
* func::fix_class_args($class, $args)
* # $class === "class"
* # $args === ["arg1", "arg2", "arg3"]
* ~~~
*
* @return bool true si la correction a été faite
*/
static final function fix_class_args(&$class, ?array &$args): bool {
if ($args === null) $args = [];
if (is_array($class)) {
if (count($class) > 1) {
$prefix_args = array_slice($class, 1);
$class = array_slice($class, 0, 1)[0];
$args = array_merge($prefix_args, $args);
} else {
$class = $class[0];
}
return true;
}
return false;
}
/**
* s'assurer que $class est une classe et renseigner le cas échéant les
* arguments.
*
* @return bool true si c'est une classe valide. il ne reste plus qu'à
* l'instancier avec {@link cons()}
*/
static final function check_class(&$class, &$args=null): bool {
if (self::is_class($class)) {
self::fix_class_args($class, $args);
return true;
}
return false;
}
/**
* Comme {@link check_class()} mais lance une exception si la classe est
* invalide
*
* @throws ValueException si $class n'est pas une classe valide
*/
static final function ensure_class(&$class, &$args=null): void {
if (!self::check_class($class, $args)) {
throw ValueException::invalid_type($class, "class");
}
}
/**
* Instancier la classe avec les arguments spécifiés.
* Adapter $args en fonction du nombre réel d'arguments du constructeur
*/
static final function cons(string $class, ...$args) {
$c = new ReflectionClass($class);
$rf = $c->getConstructor();
if ($rf === null) {
return $c->newInstance();
} else {
if (!$rf->isVariadic()) {
$minArgs = $rf->getNumberOfRequiredParameters();
$maxArgs = $rf->getNumberOfParameters();
$args = array_slice($args, 0, $maxArgs);
while (count($args) < $minArgs) {
$args[] = null;
}
}
return $c->newInstanceArgs($args);
}
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace nulib\ref\web;
class ref_mimetypes {
const TXT = "text/plain";
const CSV = "text/csv";
const BINARY = "application/octet-stream";
const XLS = "application/vnd.ms-excel";
const XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
const ODS = "application/vnd.oasis.opendocument.spreadsheet";
}

View File

@ -0,0 +1,124 @@
<?php
namespace nulib\tools;
use nulib\ExitError;
use nulib\ext\yaml;
use nulib\os\path;
use nulib\os\proc\Cmd;
use nulib\os\sh;
use nulib\output\msg;
use nulib\app;
use nulib\app\cli\Application;
use nulib\app\RunFile;
class BgLauncherApp extends Application {
const ACTION_INFOS = 0, ACTION_START = 1, ACTION_STOP = 2;
const ARGS = [
"purpose" => "lancer un script en tâche de fond",
"usage" => "ApplicationClass args...",
"sections" => [
parent::VERBOSITY_SECTION,
],
["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS,
"help" => "Afficher des informations sur la tâche",
],
["-s", "--start", "name" => "action", "value" => self::ACTION_START,
"help" => "Démarrer la tâche",
],
["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP,
"help" => "Arrêter la tâche",
],
];
protected int $action = self::ACTION_START;
protected ?array $args = null;
static function show_infos(RunFile $runfile, ?int $level=null): void {
msg::print($runfile->getDesc(), $level);
msg::print(yaml::with(["data" => $runfile->read()]), ($level ?? 0) - 1);
}
function main() {
$args = $this->args;
$appClass = $args[0] ?? null;
if ($appClass === null) {
self::die("Vous devez spécifier la classe de l'application");
}
$appClass = $args[0] = str_replace("/", "\\", $appClass);
if (!class_exists($appClass)) {
self::die("$appClass: classe non trouvée");
}
$useRunfile = constant("$appClass::USE_RUNFILE");
if (!$useRunfile) {
self::die("Cette application ne supporte le lancement en tâche de fond");
}
$runfile = app::with($appClass)->getRunfile();
switch ($this->action) {
case self::ACTION_START:
$argc = count($args);
$appClass::_manage_runfile($argc, $args, $runfile);
if ($runfile->warnIfLocked()) self::exit(app::EC_LOCKED);
array_splice($args, 0, 0, [
PHP_BINARY,
path::abspath(NULIB_APP_app_launcher),
]);
app::params_putenv();
self::_start($args, $runfile);
break;
case self::ACTION_STOP:
self::_stop($runfile);
self::show_infos($runfile, -1);
break;
case self::ACTION_INFOS:
self::show_infos($runfile);
break;
}
}
public static function _start(array $args, Runfile $runfile): void {
$pid = pcntl_fork();
if ($pid == -1) {
# parent, impossible de forker
throw new ExitError(app::EC_FORK_PARENT, "Unable to fork");
} elseif (!$pid) {
# child, fork ok
$runfile->wfPrepare($pid);
$outfile = $runfile->getOutfile() ?? "/tmp/NULIB_APP_app_console.out";
$exitcode = app::EC_FORK_CHILD;
try {
# rediriger STDIN, STDOUT et STDERR
fclose(fopen($outfile, "wb")); // vider le fichier
fclose(STDIN); $in = fopen("/dev/null", "rb");
fclose(STDOUT); $out = fopen($outfile, "ab");
fclose(STDERR); $err = fopen($outfile, "ab");
# puis lancer la commande
$cmd = new Cmd($args);
$cmd->addSource("/g/init.env");
$cmd->addRedir("both", $outfile, true);
$cmd->fork_exec($exitcode, false);
sh::_waitpid(-$pid, $exitcode);
} finally {
$runfile->wfReaped($exitcode);
}
}
}
public static function _stop(Runfile $runfile): bool {
$data = $runfile->read();
$pid = $runfile->_getCid($data);
msg::action("stop $pid");
if ($runfile->wfKill($reason)) {
msg::asuccess();
return true;
} else {
msg::afailure($reason);
return false;
}
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace nulib\tools;
use nulib\output\msg;
use nulib\php\time\DateTime;
use nulib\text\words;
use nulib\app;
use nulib\app\cli\Application;
class SteamTrainApp extends Application {
const PROJDIR = __DIR__.'/../..';
const TITLE = "Train à vapeur";
const USE_LOGFILE = true;
const USE_RUNFILE = true;
const USE_RUNLOCK = true;
const ARGS = [
"purpose" => self::TITLE,
"description" => <<<EOT
Cette application peut être utilisée pour tester le lancement des tâches de fond
EOT,
["-c", "--count", "args" => 1,
"help" => "spécifier le nombre d'étapes",
],
["-f", "--force-enabled", "value" => true,
"help" => "lancer la commande même si les tâches planifiées sont désactivées",
],
["-n", "--no-install-signal-handler", "value" => false,
"help" => "ne pas installer le gestionnaire de signaux",
],
];
protected $count = 100;
protected bool $forceEnabled = false;
protected bool $installSignalHandler = true;
function main() {
app::check_bgapplication_enabled($this->forceEnabled);
if ($this->installSignalHandler) app::install_signal_handler();
$count = intval($this->count);
msg::info("Starting train for ".words::q($count, "step#s"));
app::action("Running train...", $count);
for ($i = 1; $i <= $count; $i++) {
msg::print("Tchou-tchou! x $i");
app::step();
sleep(1);
}
msg::info("Stopping train at ".new DateTime());
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace nulib\app;
use nur\cli\Application;
use nulib\output\msg;
class LongTaskApp extends Application {
const APPCODE = "long-task";
const USE_LOGFILE = true;
const USE_RUNFILE = true;
const USE_RUNLOCK = true;
function main() {
$step = 100;
while (--$step > 0) {
msg::print("step $step");
sleep(1);
}
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace nulib\app;
use nulib\tests\TestCase;
use nulib\app\args;
class argsTest extends TestCase {
function testFrom_array() {
self::assertSame([], args::from_array(null));
self::assertSame([], args::from_array([]));
self::assertSame([], args::from_array([false]));
self::assertSame(["x"], args::from_array(["x", false]));
self::assertSame(["--opt"], args::from_array(["--opt"]));
self::assertSame(["--opt", "value"], args::from_array(["--opt", "value"]));
self::assertSame([], args::from_array(["opt" => false]));
self::assertSame(["--opt"], args::from_array(["opt" => true]));
self::assertSame(["--opt", "value"], args::from_array(["opt" => "value"]));
self::assertSame(["--opt", "42"], args::from_array(["opt" => 42]));
self::assertSame(["--opt", "1", "2", "3", "--"], args::from_array(["opt" => [1, 2, 3]]));
self::assertSame(["x", "1", "2", "3", "y"], args::from_array(["x", [1, 2, 3], "y"]));
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace nulib\app;
use nulib\tests\TestCase;
class launcherTest extends TestCase {
function testVerifix_args() {
self::assertSame([], launcher::verifix_args([]));
self::assertSame(["a"], launcher::verifix_args(["a"]));
self::assertSame(["a", "--b"], launcher::verifix_args(["a", "--b"]));
self::assertSame([], launcher::verifix_args(["a" => false]));
self::assertSame(["--a"], launcher::verifix_args(["a" => true]));
self::assertSame(["--a", "value"], launcher::verifix_args(["a" => "value"]));
self::assertSame(["--a", "52"], launcher::verifix_args(["a" => 52]));
self::assertSame(["--aa-bb", "value"], launcher::verifix_args(["aaBb" => "value"]));
self::assertSame(["--aa-bb", "value"], launcher::verifix_args(["aa-Bb" => "value"]));
self::assertSame(["--aa-bb", "value"], launcher::verifix_args(["aa_Bb" => "value"]));
self::assertSame(["---aa-bb", "value"], launcher::verifix_args(["_aa_Bb" => "value"]));
}
}

132
php/tests/appTest.php Normal file
View File

@ -0,0 +1,132 @@
<?php
namespace nulib {
use nulib\tests\TestCase;
use nulib\impl\config;
use nulib\impl\myapp;
use nulib\impl\MyApplication1;
use nulib\impl\MyApplication2;
class appTest extends TestCase {
function testWith() {
$projdir = config::get_projdir();
$cwd = getcwd();
myapp::reset();
$app1 = myapp::with(MyApplication1::class);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
"bindir" => "$projdir/vendor/bin",
"autoload" => "$projdir/vendor/autoload.php",
],
"appcode" => "nur-sery",
"cwd" => $cwd,
"datadir" => "$projdir/devel",
"etcdir" => "$projdir/devel/etc",
"vardir" => "$projdir/devel/var",
"logdir" => "$projdir/devel/log",
"profile" => "devel",
"appgroup" => null,
"name" => "my-application1",
"title" => null,
], $app1->getParams());
$app2 = myapp::with(MyApplication2::class, $app1);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
"bindir" => "$projdir/vendor/bin",
"autoload" => "$projdir/vendor/autoload.php",
],
"appcode" => "nur-sery",
"cwd" => $cwd,
"datadir" => "$projdir/devel",
"etcdir" => "$projdir/devel/etc",
"vardir" => "$projdir/devel/var",
"logdir" => "$projdir/devel/log",
"profile" => "devel",
"appgroup" => null,
"name" => "my-application2",
"title" => null,
], $app2->getParams());
}
function testInit() {
$projdir = config::get_projdir();
$cwd = getcwd();
myapp::reset();
myapp::init(MyApplication1::class);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
"bindir" => "$projdir/vendor/bin",
"autoload" => "$projdir/vendor/autoload.php",
],
"appcode" => "nur-sery",
"cwd" => $cwd,
"datadir" => "$projdir/devel",
"etcdir" => "$projdir/devel/etc",
"vardir" => "$projdir/devel/var",
"logdir" => "$projdir/devel/log",
"profile" => "devel",
"appgroup" => null,
"name" => "my-application1",
"title" => null,
], myapp::get()->getParams());
myapp::init(MyApplication2::class);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
"bindir" => "$projdir/vendor/bin",
"autoload" => "$projdir/vendor/autoload.php",
],
"appcode" => "nur-sery",
"cwd" => $cwd,
"datadir" => "$projdir/devel",
"etcdir" => "$projdir/devel/etc",
"vardir" => "$projdir/devel/var",
"logdir" => "$projdir/devel/log",
"profile" => "devel",
"appgroup" => null,
"name" => "my-application2",
"title" => null,
], myapp::get()->getParams());
}
}
}
namespace nulib\impl {
use nulib\app\cli\Application;
use nulib\os\path;
use nulib\app;
class config {
const PROJDIR = __DIR__.'/..';
static function get_projdir(): string {
return path::abspath(self::PROJDIR);
}
}
class myapp extends app {
static function reset(): void {
self::$app = null;
}
}
class MyApplication1 extends Application {
const PROJDIR = config::PROJDIR;
function main() {
}
}
class MyApplication2 extends Application {
const PROJDIR = null;
function main() {
}
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace nulib\php\access;
use nulib\tests\TestCase;
use nulib\wip\php\access\KeyAccess;
use stdClass;
class KeyAccessTest extends TestCase {
function testAccess() {
$default = new stdClass();
$array = ["null" => null, "false" => false, "empty" => ""];
#
$a = new KeyAccess($array, "inexistant");
self::assertFalse($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$a = new KeyAccess($array, "null");
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame(null, $a->get($default));
$a = new KeyAccess($array, "false");
self::assertTrue($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$a = new KeyAccess($array, "empty");
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame("", $a->get($default));
#
$a = new KeyAccess($array, "null", ["allow_null" => false]);
self::assertTrue($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$a = new KeyAccess($array, "null", ["allow_null" => true]);
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame(null, $a->get($default));
#
$a = new KeyAccess($array, "false", ["allow_false" => false]);
self::assertTrue($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$a = new KeyAccess($array, "false", ["allow_false" => true]);
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame(false, $a->get($default));
#
$a = new KeyAccess($array, "empty", ["allow_empty" => false]);
self::assertTrue($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$a = new KeyAccess($array, "empty", ["allow_empty" => true]);
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame("", $a->get($default));
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace nulib\php\access;
use nulib\tests\TestCase;
use nulib\wip\php\access\ValueAccess;
use stdClass;
class ValueAccessTest extends TestCase {
function testAccess() {
$default = new stdClass();
#
$i = null;
$a = new ValueAccess($i);
self::assertFalse($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$i = false;
$a = new ValueAccess($i);
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame(false, $a->get($default));
$i = "";
$a = new ValueAccess($i);
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame("", $a->get($default));
#
$i = null;
$a = new ValueAccess($i, ["allow_null" => false]);
self::assertFalse($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$i = null;
$a = new ValueAccess($i, ["allow_null" => true]);
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame(null, $a->get($default));
#
$i = false;
$a = new ValueAccess($i, ["allow_false" => false]);
self::assertTrue($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$i = false;
$a = new ValueAccess($i, ["allow_false" => true]);
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame(false, $a->get($default));
#
$i = "";
$a = new ValueAccess($i, ["allow_empty" => false]);
self::assertTrue($a->exists());
self::assertFalse($a->available());
self::assertSame($default, $a->get($default));
$i = "";
$a = new ValueAccess($i, ["allow_empty" => true]);
self::assertTrue($a->exists());
self::assertTrue($a->available());
self::assertSame("", $a->get($default));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,292 @@
<?php
namespace {
function func36(): int { return 36; }
function func_m1($a): array { return [$a]; }
function func_o1($b=9): array { return [$b]; }
function func_v(...$c): array { return [...$c]; }
function func_m1o1($a, $b=9): array { return [$a, $b]; }
function func_m1v($a, ...$c): array { return [$a, ...$c]; }
function func_m1o1v($a, $b=9, ...$c): array { return [$a, $b, ...$c]; }
function func_o1v($b=9, ...$c): array { return [$b, ...$c]; }
}
namespace nulib\php {
use nulib\tests\TestCase;
class nur_funcTest extends TestCase {
function testIs_static() {
self::assertFalse(nur_func::is_static(null));
self::assertFalse(nur_func::is_static(""));
self::assertFalse(nur_func::is_static("::"));
self::assertFalse(nur_func::is_static("xxx::"));
self::assertFalse(nur_func::is_static([]));
self::assertFalse(nur_func::is_static([""]));
self::assertFalse(nur_func::is_static([null, ""]));
self::assertFalse(nur_func::is_static(["xxx", ""]));
self::assertTrue(nur_func::is_static("::xxx"));
self::assertTrue(nur_func::is_static(["xxx"]));
self::assertTrue(nur_func::is_static([null, "yyy"]));
self::assertTrue(nur_func::is_static(["xxx", "yyy"]));
self::assertTrue(nur_func::is_static([null, "yyy", "aaa"]));
self::assertTrue(nur_func::is_static(["xxx", "yyy", "aaa"]));
}
function testFix_static() {
$class = "class";
$func = "::xxx";
nur_func::fix_static($func, $class);
self::assertSame("class::xxx", $func);
$func = ["xxx"];
nur_func::fix_static($func, $class);
self::assertSame(["class", "xxx"], $func);
$func = [null, "yyy"];
nur_func::fix_static($func, $class);
self::assertSame(["class", "yyy"], $func);
$func = ["xxx", "yyy"];
nur_func::fix_static($func, $class);
self::assertSame(["xxx", "yyy"], $func);
$func = [null, "yyy", "aaa"];
nur_func::fix_static($func, $class);
self::assertSame(["class", "yyy", "aaa"], $func);
$func = ["xxx", "yyy", "aaa"];
nur_func::fix_static($func, $class);
self::assertSame(["xxx", "yyy", "aaa"], $func);
}
function testIs_method() {
self::assertFalse(nur_func::is_method(null));
self::assertFalse(nur_func::is_method(""));
self::assertFalse(nur_func::is_method("->"));
self::assertFalse(nur_func::is_method([]));
self::assertFalse(nur_func::is_method([""]));
self::assertFalse(nur_func::is_method([null, "->"]));
self::assertFalse(nur_func::is_method(["xxx", "->"]));
self::assertTrue(nur_func::is_method("->xxx"));
self::assertTrue(nur_func::is_method(["->xxx"]));
self::assertTrue(nur_func::is_method([null, "->yyy"]));
self::assertTrue(nur_func::is_method(["xxx", "->yyy"]));
self::assertTrue(nur_func::is_method([null, "->yyy", "aaa"]));
self::assertTrue(nur_func::is_method(["xxx", "->yyy", "aaa"]));
}
function testFix_method() {
$object = new \stdClass();
$func= "->xxx";
nur_func::fix_method($func, $object);
self::assertSame([$object, "xxx"], $func);
$func= ["->xxx"];
nur_func::fix_method($func, $object);
self::assertSame([$object, "xxx"], $func);
$func= [null, "->yyy"];
nur_func::fix_method($func, $object);
self::assertSame([$object, "yyy"], $func);
$func= ["xxx", "->yyy"];
nur_func::fix_method($func, $object);
self::assertSame([$object, "yyy"], $func);
$func= [null, "->yyy", "aaa"];
nur_func::fix_method($func, $object);
self::assertSame([$object, "yyy", "aaa"], $func);
$func= ["xxx", "->yyy", "aaa"];
nur_func::fix_method($func, $object);
self::assertSame([$object, "yyy", "aaa"], $func);
}
function testCall() {
self::assertSame(36, nur_func::call("func36"));
self::assertSame(12, nur_func::call(TC::class."::method"));
self::assertSame(12, nur_func::call([TC::class, "method"]));
$closure = function() {
return 21;
};
self::assertSame(21, nur_func::call($closure));
}
function test_prepare_fill() {
# vérifier que les arguments sont bien remplis, en fonction du fait qu'ils
# soient obligatoires, facultatifs ou variadiques
# m1
self::assertSame([null], nur_func::call("func_m1"));
self::assertSame([null], nur_func::call("func_m1", null));
self::assertSame([null], nur_func::call("func_m1", null, null));
self::assertSame([null], nur_func::call("func_m1", null, null, null));
self::assertSame([null], nur_func::call("func_m1", null, null, null, null));
self::assertSame([1], nur_func::call("func_m1", 1));
self::assertSame([1], nur_func::call("func_m1", 1, 2));
self::assertSame([1], nur_func::call("func_m1", 1, 2, 3));
self::assertSame([1], nur_func::call("func_m1", 1, 2, 3, 4));
# o1
self::assertSame([9], nur_func::call("func_o1"));
self::assertSame([null], nur_func::call("func_o1", null));
self::assertSame([null], nur_func::call("func_o1", null, null));
self::assertSame([null], nur_func::call("func_o1", null, null, null));
self::assertSame([null], nur_func::call("func_o1", null, null, null, null));
self::assertSame([1], nur_func::call("func_o1", 1));
self::assertSame([1], nur_func::call("func_o1", 1, 2));
self::assertSame([1], nur_func::call("func_o1", 1, 2, 3));
self::assertSame([1], nur_func::call("func_o1", 1, 2, 3, 4));
# v
self::assertSame([], nur_func::call("func_v"));
self::assertSame([null], nur_func::call("func_v", null));
self::assertSame([null, null], nur_func::call("func_v", null, null));
self::assertSame([null, null, null], nur_func::call("func_v", null, null, null));
self::assertSame([null, null, null, null], nur_func::call("func_v", null, null, null, null));
self::assertSame([1], nur_func::call("func_v", 1));
self::assertSame([1, 2], nur_func::call("func_v", 1, 2));
self::assertSame([1, 2, 3], nur_func::call("func_v", 1, 2, 3));
self::assertSame([1, 2, 3, 4], nur_func::call("func_v", 1, 2, 3, 4));
# m1o1
self::assertSame([null, 9], nur_func::call("func_m1o1"));
self::assertSame([null, 9], nur_func::call("func_m1o1", null));
self::assertSame([null, null], nur_func::call("func_m1o1", null, null));
self::assertSame([null, null], nur_func::call("func_m1o1", null, null, null));
self::assertSame([null, null], nur_func::call("func_m1o1", null, null, null, null));
self::assertSame([1, 9], nur_func::call("func_m1o1", 1));
self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2));
self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2, 3));
self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2, 3, 4));
# m1v
self::assertSame([null], nur_func::call("func_m1v"));
self::assertSame([null], nur_func::call("func_m1v", null));
self::assertSame([null, null], nur_func::call("func_m1v", null, null));
self::assertSame([null, null, null], nur_func::call("func_m1v", null, null, null));
self::assertSame([null, null, null, null], nur_func::call("func_m1v", null, null, null, null));
self::assertSame([1], nur_func::call("func_m1v", 1));
self::assertSame([1, 2], nur_func::call("func_m1v", 1, 2));
self::assertSame([1, 2, 3], nur_func::call("func_m1v", 1, 2, 3));
self::assertSame([1, 2, 3, 4], nur_func::call("func_m1v", 1, 2, 3, 4));
# m1o1v
self::assertSame([null, 9], nur_func::call("func_m1o1v"));
self::assertSame([null, 9], nur_func::call("func_m1o1v", null));
self::assertSame([null, null], nur_func::call("func_m1o1v", null, null));
self::assertSame([null, null, null], nur_func::call("func_m1o1v", null, null, null));
self::assertSame([null, null, null, null], nur_func::call("func_m1o1v", null, null, null, null));
self::assertSame([1, 9], nur_func::call("func_m1o1v", 1));
self::assertSame([1, 2], nur_func::call("func_m1o1v", 1, 2));
self::assertSame([1, 2, 3], nur_func::call("func_m1o1v", 1, 2, 3));
self::assertSame([1, 2, 3, 4], nur_func::call("func_m1o1v", 1, 2, 3, 4));
# o1v
self::assertSame([9], nur_func::call("func_o1v"));
self::assertSame([null], nur_func::call("func_o1v", null));
self::assertSame([null, null], nur_func::call("func_o1v", null, null));
self::assertSame([null, null, null], nur_func::call("func_o1v", null, null, null));
self::assertSame([null, null, null, null], nur_func::call("func_o1v", null, null, null, null));
self::assertSame([1], nur_func::call("func_o1v", 1));
self::assertSame([1, 2], nur_func::call("func_o1v", 1, 2));
self::assertSame([1, 2, 3], nur_func::call("func_o1v", 1, 2, 3));
self::assertSame([1, 2, 3, 4], nur_func::call("func_o1v", 1, 2, 3, 4));
}
function testCall_all() {
$c1 = new C1();
$c2 = new C2();
$c3 = new C3();
self::assertSameValues([11, 12], nur_func::call_all(C1::class));
self::assertSameValues([11, 12, 21, 22], nur_func::call_all($c1));
self::assertSameValues([13, 11, 12], nur_func::call_all(C2::class));
self::assertSameValues([13, 23, 11, 12, 21, 22], nur_func::call_all($c2));
self::assertSameValues([111, 13, 12], nur_func::call_all(C3::class));
self::assertSameValues([111, 121, 13, 23, 12, 22], nur_func::call_all($c3));
$options = "conf";
self::assertSameValues([11], nur_func::call_all(C1::class, $options));
self::assertSameValues([11, 21], nur_func::call_all($c1, $options));
self::assertSameValues([11], nur_func::call_all(C2::class, $options));
self::assertSameValues([11, 21], nur_func::call_all($c2, $options));
self::assertSameValues([111], nur_func::call_all(C3::class, $options));
self::assertSameValues([111, 121], nur_func::call_all($c3, $options));
$options = ["prefix" => "conf"];
self::assertSameValues([11], nur_func::call_all(C1::class, $options));
self::assertSameValues([11, 21], nur_func::call_all($c1, $options));
self::assertSameValues([11], nur_func::call_all(C2::class, $options));
self::assertSameValues([11, 21], nur_func::call_all($c2, $options));
self::assertSameValues([111], nur_func::call_all(C3::class, $options));
self::assertSameValues([111, 121], nur_func::call_all($c3, $options));
self::assertSameValues([11, 12], nur_func::call_all($c1, ["include" => "x"]));
self::assertSameValues([11, 21], nur_func::call_all($c1, ["include" => "y"]));
self::assertSameValues([11, 12, 21], nur_func::call_all($c1, ["include" => ["x", "y"]]));
self::assertSameValues([21, 22], nur_func::call_all($c1, ["exclude" => "x"]));
self::assertSameValues([12, 22], nur_func::call_all($c1, ["exclude" => "y"]));
self::assertSameValues([22], nur_func::call_all($c1, ["exclude" => ["x", "y"]]));
self::assertSameValues([12], nur_func::call_all($c1, ["include" => "x", "exclude" => "y"]));
}
function testCons() {
$obj1 = nur_func::cons(WoCons::class, 1, 2, 3);
self::assertInstanceOf(WoCons::class, $obj1);
$obj2 = nur_func::cons(WithEmptyCons::class, 1, 2, 3);
self::assertInstanceOf(WithEmptyCons::class, $obj2);
$obj3 = nur_func::cons(WithCons::class, 1, 2, 3);
self::assertInstanceOf(WithCons::class, $obj3);
self::assertSame(1, $obj3->first);
}
}
class WoCons {
}
class WithEmptyCons {
function __construct() {
}
}
class WithCons {
public $first;
function __construct($first) {
$this->first = $first;
}
}
class TC {
static function method() {
return 12;
}
}
class C1 {
static function confps1_xy() {
return 11;
}
static function ps2_x() {
return 12;
}
function confp1_y() {
return 21;
}
function p2() {
return 22;
}
}
class C2 extends C1 {
static function ps3() {
return 13;
}
function p3() {
return 23;
}
}
class C3 extends C2 {
static function confps1_xy() {
return 111;
}
function confp1_y() {
return 121;
}
}
}

View File

@ -2,7 +2,8 @@
namespace nulib\schema\_scalar;
use nulib\tests\TestCase;
use nulib\schema\SchemaException;
use nulib\wip\schema\_scalar\ScalarSchema;
use nulib\wip\schema\SchemaException;
class ScalarSchemaTest extends TestCase {
const NULL_SCHEMA = [

View File

@ -3,8 +3,8 @@ namespace nulib\schema\types;
use Exception;
use nulib\tests\TestCase;
use nulib\schema\_scalar\ScalarValue;
use nulib\schema\Schema;
use nulib\wip\schema\_scalar\ScalarValue;
use nulib\wip\schema\Schema;
class strTest extends TestCase {
function commonTests($destv, &$dest, callable $destvSetter): void {