From 9a48a2bee461358064280d482849aba4518c5863 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 14 Nov 2024 20:00:00 +0400 Subject: [PATCH] maj nur/sery --- php/src/A.php | 19 + php/src/ExitError.php | 2 +- php/src/app/RunFile.php | 364 +++-- php/src/app/args.php | 39 + php/src/app/cli/include-launcher.php | 29 + php/src/app/launcher.php | 136 -- php/src/cl.php | 36 +- php/src/db/Capacitor.php | 4 +- php/src/db/CapacitorChannel.php | 2 +- php/src/db/CapacitorStorage.php | 36 +- php/src/db/_private/Tbindings.php | 36 + php/src/db/_private/Tinsert.php | 4 +- php/src/db/_private/Tvalues.php | 35 + php/src/db/pdo/Pdo.php | 39 +- php/src/db/pdo/_config.php | 8 +- php/src/db/pdo/_query_base.php | 34 +- php/src/db/sqlite/Sqlite.php | 11 +- php/src/db/sqlite/SqliteStorage.php | 63 +- php/src/db/sqlite/_config.php | 8 +- php/src/db/sqlite/_migration.php | 8 +- php/src/db/sqlite/_query_base.php | 4 + php/src/ext/json.php | 67 + php/src/{php => ext}/json/JsonException.php | 2 +- .../ext/spreadsheet/PhpSpreadsheetBuilder.php | 112 -- .../ext/spreadsheet/PhpSpreadsheetReader.php | 116 -- php/src/ext/spreadsheet/SpoutBuilder.php | 202 --- php/src/ext/spreadsheet/SpoutReader.php | 121 -- php/src/ext/spreadsheet/SsBuilder.php | 9 - php/src/ext/spreadsheet/SsReader.php | 8 - php/src/ext/spreadsheet/ssutils.php | 14 - php/src/ext/spreadsheet/wsutils.php | 80 - php/src/file.php | 9 +- php/src/file/csv/AbstractBuilder.php | 27 +- php/src/file/csv/AbstractReader.php | 38 +- php/src/file/csv/CsvReader.php | 5 +- php/src/file/csv/IBuilder.php | 2 +- php/src/file/csv/TAbstractBuilder.php | 14 +- php/src/file/csv/TAbstractReader.php | 14 +- php/src/os/proc/AbstractCmd.php | 4 +- php/src/os/sh.php | 72 +- php/src/output/msg.php | 16 +- php/src/output/std/StdMessenger.php | 10 +- php/src/php/content/c.php | 30 +- php/src/php/func.php | 1019 +++++++----- php/src/php/mprop.php | 6 +- php/src/php/nur_func.php | 453 ++++++ php/src/ref/web/ref_mimetypes.php | 12 + php/src/tools/BgLauncherApp.php | 124 ++ php/src/tools/SteamTrainApp.php | 53 + php/tests/app/LongTaskApp.php | 20 - php/tests/app/argsTest.php | 26 + php/tests/app/launcherTest.php | 20 - php/tests/appTest.php | 132 ++ php/tests/php/access/KeyAccessTest.php | 67 + php/tests/php/access/ValueAccessTest.php | 70 + php/tests/php/funcTest.php | 1383 ++++++++++++++--- php/tests/php/nur_funcTest.php | 292 ++++ php/tests/schema/_scalar/ScalarSchemaTest.php | 3 +- php/tests/schema/types/strTest.php | 4 +- 59 files changed, 3760 insertions(+), 1813 deletions(-) create mode 100644 php/src/app/args.php create mode 100644 php/src/app/cli/include-launcher.php delete mode 100644 php/src/app/launcher.php create mode 100644 php/src/db/_private/Tbindings.php create mode 100644 php/src/db/_private/Tvalues.php create mode 100644 php/src/ext/json.php rename php/src/{php => ext}/json/JsonException.php (95%) delete mode 100644 php/src/ext/spreadsheet/PhpSpreadsheetBuilder.php delete mode 100644 php/src/ext/spreadsheet/PhpSpreadsheetReader.php delete mode 100644 php/src/ext/spreadsheet/SpoutBuilder.php delete mode 100644 php/src/ext/spreadsheet/SpoutReader.php delete mode 100644 php/src/ext/spreadsheet/SsBuilder.php delete mode 100644 php/src/ext/spreadsheet/SsReader.php delete mode 100644 php/src/ext/spreadsheet/ssutils.php delete mode 100644 php/src/ext/spreadsheet/wsutils.php create mode 100644 php/src/php/nur_func.php create mode 100644 php/src/ref/web/ref_mimetypes.php create mode 100644 php/src/tools/BgLauncherApp.php create mode 100644 php/src/tools/SteamTrainApp.php delete mode 100644 php/tests/app/LongTaskApp.php create mode 100644 php/tests/app/argsTest.php delete mode 100644 php/tests/app/launcherTest.php create mode 100644 php/tests/appTest.php create mode 100644 php/tests/php/access/KeyAccessTest.php create mode 100644 php/tests/php/access/ValueAccessTest.php create mode 100644 php/tests/php/nur_funcTest.php diff --git a/php/src/A.php b/php/src/A.php index ef96c45..c7d982b 100644 --- a/php/src/A.php +++ b/php/src/A.php @@ -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)); + } } diff --git a/php/src/ExitError.php b/php/src/ExitError.php index b08cdd6..a14c3a8 100644 --- a/php/src/ExitError.php +++ b/php/src/ExitError.php @@ -21,7 +21,7 @@ class ExitError extends Error { /** @var ?string */ protected $userMessage; - function haveMessage(): bool { + function haveUserMessage(): bool { return $this->userMessage !== null; } diff --git a/php/src/app/RunFile.php b/php/src/app/RunFile.php index 12460a4..d76beb9 100644 --- a/php/src/app/RunFile.php +++ b/php/src/app/RunFile.php @@ -1,11 +1,15 @@ 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"]) { - return false; + 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; } - $done = true; - if ($updateDone) return ["is_done" => $done]; - else return null; - }); - return $done; + return false; + } + 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, - ]; - }); - } - - /** indiquer qu'une étape est franchie dans l'action en cours */ - function step(int $nbSteps=1): void { - $this->update(function (array $data) use ($nbSteps) { - return [ - "action_date_step" => new DateTime(), - "action_current_step" => $data["action_current_step"] + $nbSteps, - ]; - }); - } - - #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Divers - - function getLockFile(?string $name=null, ?string $title=null): LockFile { - $ext = self::LOCK_EXT; - if ($name !== null) $ext = ".$name$ext"; - $file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT); - $name = str::join("/", [$this->name, $name]); - return new LockFile($file, $name, $title); - } - - #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Gestionnaire de tâches (tm_*) - - /** démarrer un groupe de process dont le process courant est le leader */ - function tm_startPg(): void { - $this->update(function (array $data) { - posix_setsid(); - return [ - "pg_pid" => posix_getpid(), - ]; - }); + 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; + } + } + 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; + } + } + } + if ($stopped) { + sh::_waitpid($pid, $exitcode); + $this->wfReaped($exitcode); + } + return $stopped; } /** * vérifier si on est dans le cas où la tâche devrait tourner mais en réalité * ce n'est pas le cas */ - function tm_isUndead(?int $pid=null): bool { + 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), + ]; } } diff --git a/php/src/app/args.php b/php/src/app/args.php new file mode 100644 index 0000000..90e24c7 --- /dev/null +++ b/php/src/app/args.php @@ -0,0 +1,39 @@ + $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; + } +} diff --git a/php/src/app/cli/include-launcher.php b/php/src/app/cli/include-launcher.php new file mode 100644 index 0000000..99ebabf --- /dev/null +++ b/php/src/app/cli/include-launcher.php @@ -0,0 +1,29 @@ + $name, + ]); + require $app; +} diff --git a/php/src/app/launcher.php b/php/src/app/launcher.php deleted file mode 100644 index 8ec5216..0000000 --- a/php/src/app/launcher.php +++ /dev/null @@ -1,136 +0,0 @@ - $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(); - } -} diff --git a/php/src/cl.php b/php/src/cl.php index 448f6a5..8bb3b37 100644 --- a/php/src/cl.php +++ b/php/src/cl.php @@ -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; } } diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php index 70c6f46..90c3c9b 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -1,7 +1,7 @@ commit(); $commited = true; diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php index 8ce832f..4495074 100644 --- a/php/src/db/CapacitorChannel.php +++ b/php/src/db/CapacitorChannel.php @@ -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; diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index a257aee..ec27fae 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -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", diff --git a/php/src/db/_private/Tbindings.php b/php/src/db/_private/Tbindings.php new file mode 100644 index 0000000..c8ee8eb --- /dev/null +++ b/php/src/db/_private/Tbindings.php @@ -0,0 +1,36 @@ +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; + } + } +} diff --git a/php/src/db/_private/Tinsert.php b/php/src/db/_private/Tinsert.php index 234add3..272c9e9 100644 --- a/php/src/db/_private/Tinsert.php +++ b/php/src/db/_private/Tinsert.php @@ -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); diff --git a/php/src/db/_private/Tvalues.php b/php/src/db/_private/Tvalues.php new file mode 100644 index 0000000..ad93f5c --- /dev/null +++ b/php/src/db/_private/Tvalues.php @@ -0,0 +1,35 @@ +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); diff --git a/php/src/db/pdo/_config.php b/php/src/db/pdo/_config.php index 8b14b8f..5055d6f 100644 --- a/php/src/db/pdo/_config.php +++ b/php/src/db/pdo/_config.php @@ -1,7 +1,7 @@ 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); } } } diff --git a/php/src/db/pdo/_query_base.php b/php/src/db/pdo/_query_base.php index be7cefb..921704d 100644 --- a/php/src/db/pdo/_query_base.php +++ b/php/src/db/pdo/_query_base.php @@ -1,14 +1,13 @@ 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 { diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index a45afd2..a1ebf5c 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -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; diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index 820256f..287a9f7 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -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", + 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", + ], + ]); + } + 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->getTableName(), + "name" => $channel->getName(), ], ]); - return $tableName !== null; + } + + function _exists(CapacitorChannel $channel): bool { + return $this->tableExists($channel->getTableName()); } function close(): void { diff --git a/php/src/db/sqlite/_config.php b/php/src/db/sqlite/_config.php index f0ef2ed..ea7553a 100644 --- a/php/src/db/sqlite/_config.php +++ b/php/src/db/sqlite/_config.php @@ -1,7 +1,7 @@ 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); } } } diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php index 6f80f04..d2adf93 100644 --- a/php/src/db/sqlite/_migration.php +++ b/php/src/db/sqlite/_migration.php @@ -1,7 +1,7 @@ $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, diff --git a/php/src/db/sqlite/_query_base.php b/php/src/db/sqlite/_query_base.php index 8a6128a..9545077 100644 --- a/php/src/db/sqlite/_query_base.php +++ b/php/src/db/sqlite/_query_base.php @@ -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; diff --git a/php/src/ext/json.php b/php/src/ext/json.php new file mode 100644 index 0000000..68316f8 --- /dev/null +++ b/php/src/ext/json.php @@ -0,0 +1,67 @@ +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)); + } +} diff --git a/php/src/php/json/JsonException.php b/php/src/ext/json/JsonException.php similarity index 95% rename from php/src/php/json/JsonException.php rename to php/src/ext/json/JsonException.php index 97c3d8c..a534188 100644 --- a/php/src/php/json/JsonException.php +++ b/php/src/ext/json/JsonException.php @@ -1,5 +1,5 @@ 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; - } -} diff --git a/php/src/ext/spreadsheet/PhpSpreadsheetReader.php b/php/src/ext/spreadsheet/PhpSpreadsheetReader.php deleted file mode 100644 index 02c1138..0000000 --- a/php/src/ext/spreadsheet/PhpSpreadsheetReader.php +++ /dev/null @@ -1,116 +0,0 @@ - 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++; - } - } -} diff --git a/php/src/ext/spreadsheet/SpoutBuilder.php b/php/src/ext/spreadsheet/SpoutBuilder.php deleted file mode 100644 index 7a6b09f..0000000 --- a/php/src/ext/spreadsheet/SpoutBuilder.php +++ /dev/null @@ -1,202 +0,0 @@ -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; - } -} diff --git a/php/src/ext/spreadsheet/SpoutReader.php b/php/src/ext/spreadsheet/SpoutReader.php deleted file mode 100644 index 48aa0df..0000000 --- a/php/src/ext/spreadsheet/SpoutReader.php +++ /dev/null @@ -1,121 +0,0 @@ -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(); - } - } -} diff --git a/php/src/ext/spreadsheet/SsBuilder.php b/php/src/ext/spreadsheet/SsBuilder.php deleted file mode 100644 index 17d209d..0000000 --- a/php/src/ext/spreadsheet/SsBuilder.php +++ /dev/null @@ -1,9 +0,0 @@ -getAllSheets() as $ws) { - $max_coords[$ws->getTitle()] = wsutils::compute_max_coords($ws); - } - return $max_coords; - } -} diff --git a/php/src/ext/spreadsheet/wsutils.php b/php/src/ext/spreadsheet/wsutils.php deleted file mode 100644 index 7fb33e1..0000000 --- a/php/src/ext/spreadsheet/wsutils.php +++ /dev/null @@ -1,80 +0,0 @@ -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]; - } -} diff --git a/php/src/file.php b/php/src/file.php index 31334fe..bae8159 100644 --- a/php/src/file.php +++ b/php/src/file.php @@ -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); diff --git a/php/src/file/csv/AbstractBuilder.php b/php/src/file/csv/AbstractBuilder.php index 3c0a586..24212b6 100644 --- a/php/src/file/csv/AbstractBuilder.php +++ b/php/src/file/csv/AbstractBuilder.php @@ -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 ($headers !== null) $this->headers = $headers; - else $this->ensureHeaders(); - if ($this->headers !== null) $this->_write($this->headers); + 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) { diff --git a/php/src/file/csv/AbstractReader.php b/php/src/file/csv/AbstractReader.php index b52123e..0f1f84f 100644 --- a/php/src/file/csv/AbstractReader.php +++ b/php/src/file/csv/AbstractReader.php @@ -5,11 +5,14 @@ use nulib\A; use nulib\php\time\Date; use nulib\php\time\DateTime; -abstract class AbstractReader implements IReader { +abstract class AbstractReader implements IReader { const SCHEMA = null; 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 diff --git a/php/src/file/csv/CsvReader.php b/php/src/file/csv/CsvReader.php index a827d85..d05b2e0 100644 --- a/php/src/file/csv/CsvReader.php +++ b/php/src/file/csv/CsvReader.php @@ -1,6 +1,7 @@ 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++; } diff --git a/php/src/file/csv/IBuilder.php b/php/src/file/csv/IBuilder.php index ff2ca94..fae647a 100644 --- a/php/src/file/csv/IBuilder.php +++ b/php/src/file/csv/IBuilder.php @@ -1,7 +1,7 @@ getCmd(null, true); - sh::_fork_exec($cmd, $retcode); + sh::_fork_exec($cmd, $retcode, $wait); return $retcode == 0; } } diff --git a/php/src/os/sh.php b/php/src/os/sh.php index 9cc07c0..a18f012 100644 --- a/php/src/os/sh.php +++ b/php/src/os/sh.php @@ -1,7 +1,9 @@ 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); diff --git a/php/src/php/content/c.php b/php/src/php/content/c.php index d2ff6ec..9506835 100644 --- a/php/src/php/content/c.php +++ b/php/src/php/content/c.php @@ -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) { diff --git a/php/src/php/func.php b/php/src/php/func.php index e2ba76a..63ea334 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -2,452 +2,645 @@ namespace nulib\php; use Closure; -use nulib\cl; -use nulib\ref\php\ref_func; +use Exception; +use nulib\A; +use nulib\cv; +use nulib\StateException; use nulib\ValueException; use ReflectionClass; use ReflectionFunction; use ReflectionMethod; /** - * Class func: outils pour appeler des fonctions et méthodes dynamiquement + * Class func: outils pour appeler fonctions et méthodes dynamiquement + * + * les types de fonctions supportés sont: + * - fonctions (globales ou dans un namespace) + * - classes (l'appel de cette "fonction" provoque l'instanciation de la classe) + * - méthodes (statiques ou non, liables à une classe ou un objet) + * - Closure */ class func { - /** - * tester si $func est une chaine de la forme "XXX::method" où 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; + private static function _is_invalid(?string $f): bool { + return $f === null || $f === "" || $f === "::" || $f === "->"; } - /** - * 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; + private static function _is_nfunction(?string $f): bool { + return strpos($f, "\\") !== 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 final 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; - } - } + private static function _parse_static(?string &$m): bool { + $pos = strpos($m, "::"); + if ($pos === false) return false; + $m = substr($m, $pos + 2); return true; } - /** @var Schema */ - private static $call_all_params_schema; + private static function _parse_method(?string &$m): bool { + $pos = strpos($m, "->"); + if ($pos === false) return false; + $m = substr($m, $pos + 2); + return true; + } + + ############################################################################# + # Fonctions /** - * 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} + * vérifier que $func est une fonction et la normaliser le cas échéant. + * retourner true si c'est une fonction, false sinon + * + * les formes suivantes sont supportées: + * - "function" si une classe du même nom n'existe pas déjà + * - [false, "function", ...$args] c'est la forme normalisée + * + * @param bool $strict vérifier l'inexistence de la classe et l'existence de + * la fonction (ne pas uniquement faire une vérification syntaxique) */ - 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]; + static function verifix_function(&$func, bool $strict=true, ?string &$reason=null): bool { + if ($strict) { + $msg = var_export($func, true); + $reason = null; } - 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; + if ($func instanceof ReflectionFunction) return true; + if (is_string($func)) { + $c = false; + $f = $func; + } elseif (is_array($func)) { + if (!array_key_exists(0, $func)) return false; + $c = $func[0]; + if (!array_key_exists(1, $func)) return false; + $f = $func[1]; } else { - throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet"); + return false; } - $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" où 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]; + if ($c !== false) return false; + if (!is_string($f)) return false; + if (self::_is_invalid($f)) return false; + if (self::_parse_static($f)) return false; + if (self::_parse_method($f)) return false; + if ($strict) { + $reason = null; + if (class_exists($f)) { + $reason = "$msg: is a class"; + return false; + } + if (!function_exists($f)) { + $reason = "$msg: function not found"; + return false; } - return true; } - return false; + $func = [false, $f]; + return true; } /** - * s'assurer que $class est une classe et renseigner le cas échéant les - * arguments. + * vérifier que $func est une fonction avec les règles de + * {@link self::verifix_function()} + */ + static function is_function($func, bool $strict=true, ?string &$reason=null): bool { + return self::verifix_function($func, $strict, $reason); + } + + ############################################################################# + # Classes + + /** + * vérifier que $func est une classe et la normaliser le cas échéant. + * retourner true si c'est une classe, false sinon * - * @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 + * les formes suivantes sont supportées: + * - "class" + * - ["class", false, ...$args] c'est la forme normalisée * - * @throws ValueException si $class n'est pas une classe valide + * @param bool $strict vérifier l'existence de la classe (ne pas uniquement + * faire une vérification syntaxique) */ - static final function ensure_class(&$class, &$args=null): void { - if (!self::check_class($class, $args)) { - throw ValueException::invalid_type($class, "class"); + static function verifix_class(&$func, bool $strict=true, ?string &$reason=null): bool { + if ($strict) { + $msg = var_export($func, true); + $reason = null; } - } - - /** - * 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(); + if ($func instanceof ReflectionClass) return true; + if (is_string($func)) { + $c = $func; + $f = false; + } elseif (is_array($func)) { + if (!array_key_exists(0, $func)) return false; + $c = $func[0]; + if (!array_key_exists(1, $func)) return false; + $f = $func[1]; } else { - if (!$rf->isVariadic()) { - $minArgs = $rf->getNumberOfRequiredParameters(); - $maxArgs = $rf->getNumberOfParameters(); - $args = array_slice($args, 0, $maxArgs); - while (count($args) < $minArgs) { - $args[] = null; + return false; + } + if (!is_string($c)) return false; + if (self::_is_invalid($c)) return false; + if (self::_parse_static($c)) return false; + if (self::_parse_method($c)) return false; + if ($f !== false) return false; + if ($strict) { + if (!class_exists($c)) { + $reason = "$msg: class not found"; + return false; + } + } + $func = [$c, false]; + return true; + } + + /** + * vérifier que $func est une classe avec les règles de + * {@link self::verifix_class()} + */ + static function is_class($func, bool $strict=true, ?string &$reason=null): bool { + return self::verifix_class($func, $strict, $reason); + } + + ############################################################################# + # Méthodes statiques + + private static function _parse_class_s(?string $cs, ?string &$c, ?string &$s): bool { + if (self::_is_invalid($cs) || self::_parse_method($cs)) return false; + $pos = strpos($cs, "::"); + if ($pos === false) return false; + if ($pos === 0) return false; + $tmpc = substr($cs, 0, $pos); + $cs = substr($cs, $pos + 2); + if (self::_is_nfunction($cs)) return false; + [$c, $s] = [$tmpc, cv::vn($cs)]; + return true; + } + + private static function _parse_c_static(?string $cs, ?string &$c, ?string &$s, ?bool &$bound): bool { + if (self::_is_invalid($cs) || self::_parse_method($cs)) return false; + $pos = strpos($cs, "::"); + if ($pos === false) return false; + if ($pos == strlen($cs) - 2) return false; + if ($pos > 0) { + $tmpc = substr($cs, 0, $pos); + $bound = true; + } else { + $tmpc = null; + $bound = false; + } + $cs = substr($cs, $pos + 2); + if (self::_is_nfunction($cs)) return false; + [$c, $s] = [$tmpc, cv::vn($cs)]; + return true; + } + + /** + * vérifier que $func est une méthode statique, et la normaliser le cas + * échéant. retourner true si c'est une méthode statique, false sinon + * + * les formes suivantes sont supportées (XXX étant null ou n'importe quelle + * valeur scalaire de n'importe quel type sauf false) + * - "XXX::function" + * - ["XXX::function", ...$args] + * - [XXX, "::function", ...$args] + * - [XXX, "function", ...$args] c'est la forme normalisée + * + * Si XXX est une classe, la méthode statique est liée. sinon, elle doit être + * liée à une classe avant d'être utilisée + * + * @param bool $strict vérifier l'existence de la classe et de la méthode si + * la méthode est liée (ne pas uniquement faire une vérification syntaxique) + */ + static function verifix_static(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + if ($strict) { + $msg = var_export($func, true); + $reason = null; + } + if ($func instanceof ReflectionMethod) { + $bound = false; + return true; + } + if (is_string($func)) { + if (!self::_parse_c_static($func, $c, $f, $bound)) return false; + $cf = [$c, $f]; + } elseif (is_array($func)) { + $cf = $func; + if (!array_key_exists(0, $cf)) return false; + $c = $cf[0]; + if ($c === false) return false; + if (is_object($c)) $c = get_class($c); + if (is_string($c)) { + if (self::_is_invalid($c)) return false; + if (self::_parse_class_s($c, $c, $f)) { + $cf[0] = $c; + if ($f !== null) { + # ["class::method"] --> ["class", "method"] + array_splice($cf, 1, 0, [$f]); + } + $bound = true; + } elseif (self::_parse_c_static($c, $c, $f, $bound)) { + # ["::method"] --> [null, "method"] + array_splice($cf, 0, 0, [null]); + $cf[1] = $f; + } else { + $cf[0] = $c; + $bound = is_string($c); } + } else { + $cf[0] = null; + $bound = false; } - return $c->newInstanceArgs($args); + # + if (!array_key_exists(1, $cf)) return false; + $f = $cf[1]; + if (!is_string($f)) return false; + if (self::_parse_c_static($f, $rc, $f, $rbound)) { + if ($rc !== null && $c === null) { + $c = $rc; + $bound = $rbound; + } + } else { + if (self::_is_invalid($f)) return false; + if (self::_is_nfunction($f)) return false; + if (self::_parse_method($f)) return false; + self::_parse_static($f); + } + $cf[1] = $f; + } else { + return false; + } + if ($strict) { + $reason = null; + if ($bound) { + if (!class_exists($c)) { + $reason = "$msg: class not found"; + return false; + } + if (!method_exists($c, $f)) { + $reason = "$msg: method not found"; + return false; + } + } else { + $reason = "$msg: not bound"; + } + } + $func = $cf; + return true; + } + + /** + * vérifier que $func est une méthode statique avec les règles de + * {@link self::verifix_static()} + */ + static function is_static($func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + return self::verifix_static($func, $strict, $bound, $reason); + } + + ############################################################################# + # Méthodes non statiques + + private static function _parse_class_m(?string $cm, ?string &$c, ?string &$m): bool { + if (self::_is_invalid($cm) || self::_parse_static($cm)) return false; + $pos = strpos($cm, "->"); + if ($pos === false) return false; + if ($pos === 0) return false; + $tmpc = substr($cm, 0, $pos); + $cm = substr($cm, $pos + 2); + if (self::_is_nfunction($cm)) return false; + [$c, $m] = [$tmpc, cv::vn($cm)]; + return true; + } + + private static function _parse_c_method(?string $cm, ?string &$c, ?string &$m, ?bool &$bound): bool { + if (self::_is_invalid($cm) || self::_parse_static($cm)) return false; + $pos = strpos($cm, "->"); + if ($pos === false) return false; + if ($pos == strlen($cm) - 2) return false; + if ($pos > 0) { + $tmpc = substr($cm, 0, $pos); + $bound = true; + } else { + $tmpc = null; + $bound = false; + } + $cm = substr($cm, $pos + 2); + if (self::_is_nfunction($cm)) return false; + [$c, $m] = [$tmpc, cv::vn($cm)]; + return true; + } + + /** + * vérifier que $func est une méthode non statique, et la normaliser le cas + * échéant. retourner true si c'est une méthode non statique, false sinon + * + * les formes suivantes sont supportées (XXX étant null ou n'importe quelle + * valeur scalaire de n'importe quel type sauf false) + * - "XXX->function" + * - ["XXX->function", ...$args] + * - [XXX, "->function", ...$args] + * - [XXX, "function", ...$args] c'est la forme normalisée + * + * Si XXX est une classe ou un objet, la méthode est liée. dans tous les cas, + * elle doit être liée à un objet avant d'être utilisée + * + * @param bool $strict vérifier l'existence de la classe et de la méthode si + * la méthode est liée (ne pas uniquement faire une vérification syntaxique) + */ + static function verifix_method(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + if ($strict) { + $msg = var_export($func, true); + $reason = null; + } + if ($func instanceof ReflectionMethod) { + $bound = false; + return true; + } + if (is_string($func)) { + if (!self::_parse_c_method($func, $c, $f, $bound)) return false; + $cf = [$c, $f]; + } elseif (is_array($func)) { + $cf = $func; + if (!array_key_exists(0, $cf)) return false; + $c = $cf[0]; + if ($c === false) return false; + if (is_object($c)) { + $bound = true; + } elseif (is_string($c)) { + if (self::_is_invalid($c)) return false; + if (self::_parse_class_m($c, $c, $f)) { + $cf[0] = $c; + if ($f !== null) { + # ["class->method"] --> ["class", "method"] + array_splice($cf, 1, 0, [$f]); + } + $bound = true; + } elseif (self::_parse_c_method($c, $c, $f, $bound)) { + # ["->method"] --> [null, "method"] + array_splice($cf, 0, 0, [null]); + $cf[1] = $f; + } else { + $cf[0] = $c; + $bound = is_string($c); + } + } else { + $cf[0] = null; + $bound = false; + } + # + if (!array_key_exists(1, $cf)) return false; + $f = $cf[1]; + if (!is_string($f)) return false; + if (self::_parse_c_method($f, $rc, $f, $rbound)) { + if ($rc !== null && $c === null) { + $c = $rc; + $bound = $rbound; + } + } else { + if (self::_is_invalid($f)) return false; + if (self::_is_nfunction($f)) return false; + if (self::_parse_static($f)) return false; + self::_parse_method($f); + } + $cf[1] = $f; + } else { + return false; + } + if ($strict) { + $reason = null; + if ($bound) { + if (!is_object($c) && !class_exists($c)) { + $reason = "$msg: class not found"; + return false; + } + if (!method_exists($c, $f)) { + $reason = "$msg: method not found"; + return false; + } + } else { + $reason = "$msg: not bound"; + } + } + $func = $cf; + return true; + } + + /** + * vérifier que $func est une méthode non statique avec les règles de + * {@link self::verifix_method()} + */ + static function is_method($func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + return self::verifix_method($func, $strict, $bound, $reason); + } + + ############################################################################# + # func + + const TYPE_MASK = 0b11; + + const FLAG_STATIC = 0b100; + + const TYPE_CLOSURE = 0, TYPE_FUNCTION = 1, TYPE_CLASS = 2, TYPE_METHOD = 3; + + const TYPE_STATIC = self::TYPE_METHOD | self::FLAG_STATIC; + + protected static function not_a_callable($func, ?string $reason) { + if ($reason === null) { + $msg = var_export($func, true); + $reason = "$msg: not a callable"; + } + return new ValueException($reason); + } + + static function with($func, ?array $args=null, bool $strict=true): self { + if (!is_array($func)) { + if ($func instanceof Closure) { + return new self(self::TYPE_CLOSURE, $func, $args); + } elseif ($func instanceof ReflectionFunction) { + return new self(self::TYPE_FUNCTION, $func, $args); + } elseif ($func instanceof ReflectionClass) { + return new self(self::TYPE_CLASS, $func, $args); + } elseif ($func instanceof ReflectionMethod) { + return new self(self::TYPE_METHOD, $func, $args, false); + } + } + if (self::verifix_function($func, $strict, $reason)) { + return new self(self::TYPE_FUNCTION, $func, $args, false, $reason); + } elseif (self::verifix_class($func, $strict, $reason)) { + return new self(self::TYPE_CLASS, $func, $args, false, $reason); + } elseif (self::verifix_method($func, $strict, $bound, $reason)) { + return new self(self::TYPE_METHOD, $func, $args, $bound, $reason); + } elseif (self::verifix_static($func, $strict, $bound, $reason)) { + return new self(self::TYPE_STATIC, $func, $args, $bound, $reason); + } + throw self::not_a_callable($func, $reason); + } + + static function ensure($func, ?array $args=null, bool $strict=true): self { + $func = self::with($func, $args, $strict); + if (!$func->isBound()) { + throw self::not_a_callable($func->func, $func->reason); + } + return $func; + } + + static function check($func, ?array $args=null, bool $strict=true): bool { + try { + self::ensure($func, $args, $strict); + return true; + } catch (Exception $e) { + return false; + } + } + + static function call($func, ...$args) { + return self::with($func)->invoke($args); + } + + ############################################################################# + + protected function __construct(int $type, $func, ?array $args=null, bool $bound=false, ?string $reason=null) { + $flags = $type & ~self::TYPE_MASK; + $type = $type & self::TYPE_MASK; + $object = null; + $prefixArgs = []; + if (!is_array($func)) { + $reflection = $func; + $func = null; + } else { + if (count($func) > 2) { + $prefixArgs = array_slice($func, 2); + $func = array_slice($func, 0, 2); + } + [$c, $f] = $func; + switch ($type) { + case self::TYPE_FUNCTION: + $reflection = new ReflectionFunction($f); + break; + case self::TYPE_CLASS: + $reflection = new ReflectionClass($c); + break; + case self::TYPE_METHOD: + if ($c === null) { + $reflection = null; + } else { + $reflection = new ReflectionMethod($c, $f); + if (is_object($c)) $object = $c; + } + break; + default: + throw StateException::unexpected_state(); + } + } + A::merge($prefixArgs, $args); + + $this->type = $type; + $this->flags = $flags; + $this->func = $func; + $this->bound = $bound; + $this->reason = $reason; + $this->object = $object; + $this->prefixArgs = $prefixArgs; + $this->updateReflection($reflection); + } + + protected int $type; + + protected int $flags; + + protected ?array $func; + + protected bool $bound; + + protected ?string $reason; + + protected ?object $object; + + protected array $prefixArgs; + + /** @var Closure|ReflectionFunction|ReflectionMethod|ReflectionClass */ + protected $reflection; + + protected bool $variadic; + + protected int $minArgs; + + protected int $maxArgs; + + protected function updateReflection($reflection): void { + $variadic = false; + $minArgs = $maxArgs = 0; + if ($reflection instanceof Closure) { + $r = new ReflectionFunction($reflection); + $variadic = $r->isVariadic(); + $minArgs = $r->getNumberOfRequiredParameters(); + $maxArgs = $r->getNumberOfParameters(); + } elseif ($reflection instanceof ReflectionClass) { + $r = $reflection->getConstructor(); + if ($r === null) { + $variadic = false; + $minArgs = $maxArgs = 0; + } else { + $variadic = $r->isVariadic(); + $minArgs = $r->getNumberOfRequiredParameters(); + $maxArgs = $r->getNumberOfParameters(); + } + } elseif ($reflection !== null) { + $variadic = $reflection->isVariadic(); + $minArgs = $reflection->getNumberOfRequiredParameters(); + $maxArgs = $reflection->getNumberOfParameters(); + } + $this->reflection = $reflection; + $this->variadic = $variadic; + $this->minArgs = $minArgs; + $this->maxArgs = $maxArgs; + } + + function isBound(): bool { + if ($this->type !== self::TYPE_METHOD) return true; + if ($this->flags & self::FLAG_STATIC) return $this->bound; + else return $this->bound && $this->object !== null; + } + + function bind($object): self { + if ($this->type !== self::TYPE_METHOD) return $this; + + [$c, $f] = $this->func; + if ($this->reflection === null) { + $this->func[0] = $c = $object; + $this->updateReflection(new ReflectionMethod($c, $f)); + } + if (is_object($object) && !($this->flags & self::FLAG_STATIC)) { + if (is_object($c)) $c = get_class($c); + if (is_string($c) && !($object instanceof $c)) { + throw ValueException::invalid_type($object, $c); + } + $this->object = $object; + $this->bound = true; + } + return $this; + } + + function invoke(?array $args=null) { + $args = array_merge($this->prefixArgs, $args ?? []); + if (!$this->variadic) $args = array_slice($args, 0, $this->maxArgs); + $minArgs = $this->minArgs; + while (count($args) < $minArgs) $args[] = null; + + switch ($this->type) { + case self::TYPE_CLOSURE: + /** @var Closure $closure */ + $closure = $this->reflection; + return $closure(...$args); + case self::TYPE_FUNCTION: + /** @var ReflectionFunction $function */ + $function = $this->reflection; + return $function->invoke(...$args); + case self::TYPE_METHOD: + /** @var ReflectionMethod $method */ + $method = $this->reflection; + if ($method === null) throw self::not_a_callable($this->func, $this->reason); + return $method->invoke($this->object, ...$args); + case self::TYPE_CLASS: + /** @var ReflectionClass $class */ + $class = $this->reflection; + return $class->newInstance(...$args); + default: + throw StateException::unexpected_state(); } } } diff --git a/php/src/php/mprop.php b/php/src/php/mprop.php index 61e764f..a0bc28d 100644 --- a/php/src/php/mprop.php +++ b/php/src/php/mprop.php @@ -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; } diff --git a/php/src/php/nur_func.php b/php/src/php/nur_func.php new file mode 100644 index 0000000..ba4cc06 --- /dev/null +++ b/php/src/php/nur_func.php @@ -0,0 +1,453 @@ + 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" où 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); + } + } +} diff --git a/php/src/ref/web/ref_mimetypes.php b/php/src/ref/web/ref_mimetypes.php new file mode 100644 index 0000000..d896c13 --- /dev/null +++ b/php/src/ref/web/ref_mimetypes.php @@ -0,0 +1,12 @@ + "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; + } + } +} diff --git a/php/src/tools/SteamTrainApp.php b/php/src/tools/SteamTrainApp.php new file mode 100644 index 0000000..3827fe4 --- /dev/null +++ b/php/src/tools/SteamTrainApp.php @@ -0,0 +1,53 @@ + self::TITLE, + "description" => << 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()); + } +} diff --git a/php/tests/app/LongTaskApp.php b/php/tests/app/LongTaskApp.php deleted file mode 100644 index d98bdec..0000000 --- a/php/tests/app/LongTaskApp.php +++ /dev/null @@ -1,20 +0,0 @@ - 0) { - msg::print("step $step"); - sleep(1); - } - } -} diff --git a/php/tests/app/argsTest.php b/php/tests/app/argsTest.php new file mode 100644 index 0000000..90252d9 --- /dev/null +++ b/php/tests/app/argsTest.php @@ -0,0 +1,26 @@ + 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"])); + } +} diff --git a/php/tests/app/launcherTest.php b/php/tests/app/launcherTest.php deleted file mode 100644 index 9f07692..0000000 --- a/php/tests/app/launcherTest.php +++ /dev/null @@ -1,20 +0,0 @@ - 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"])); - } -} diff --git a/php/tests/appTest.php b/php/tests/appTest.php new file mode 100644 index 0000000..8d86b6f --- /dev/null +++ b/php/tests/appTest.php @@ -0,0 +1,132 @@ + $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() { + } + } +} diff --git a/php/tests/php/access/KeyAccessTest.php b/php/tests/php/access/KeyAccessTest.php new file mode 100644 index 0000000..dc5bd4c --- /dev/null +++ b/php/tests/php/access/KeyAccessTest.php @@ -0,0 +1,67 @@ + 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)); + } +} diff --git a/php/tests/php/access/ValueAccessTest.php b/php/tests/php/access/ValueAccessTest.php new file mode 100644 index 0000000..a7d08c9 --- /dev/null +++ b/php/tests/php/access/ValueAccessTest.php @@ -0,0 +1,70 @@ +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)); + } +} diff --git a/php/tests/php/funcTest.php b/php/tests/php/funcTest.php index a59ea83..3f580fa 100644 --- a/php/tests/php/funcTest.php +++ b/php/tests/php/funcTest.php @@ -1,292 +1,1167 @@ ", + false, null, + false, null, + ], + ["tsimple", + true, [false, "tsimple"], + true, [false, "tsimple"], + ], + ['nulib\php\impl\ntsimple', + true, [false, 'nulib\php\impl\ntsimple'], + true, [false, 'nulib\php\impl\ntsimple'], + ], + ['tmissing', + false, null, + true, [false, 'tmissing'], + ], + ["::tstatic", + false, null, + false, null, + ], + ["->tmethod", + false, null, + false, null, + ], + ["::tmissing", + false, null, + false, null, + ], + ["->tmissing", + false, null, + false, null, + ], + ["xxx::tmissing", + false, null, + false, null, + ], + ["xxx->tmissing", + false, null, + false, null, + ], + [SC::class."::tstatic", + false, null, + false, null, + ], + [SC::class."->tmethod", + false, null, + false, null, + ], + [SC::class."::tmissing", + false, null, + false, null, + ], + [SC::class."->tmissing", + false, null, + false, null, + ], + # tableaux avec un seul scalaire + [[], + false, null, + false, null, + ], + [[null], + false, null, + false, null, + ], + [[false], + false, null, + false, null, + ], + [[""], + false, null, + false, null, + ], + [["::"], + false, null, + false, null, + ], + [["->"], + false, null, + false, null, + ], + [["tsimple"], + false, null, + false, null, + ], + [['nulib\php\impl\ntsimple'], + false, null, + false, null, + ], + [["::tstatic"], + false, null, + false, null, + ], + [["->tmethod"], + false, null, + false, null, + ], + [["::tmissing"], + false, null, + false, null, + ], + [["->tmissing"], + false, null, + false, null, + ], + [["xxx::tmissing"], + false, null, + false, null, + ], + [["xxx->tmissing"], + false, null, + false, null, + ], + [[SC::class."::tstatic"], + false, null, + false, null, + ], + [[SC::class."->tmethod"], + false, null, + false, null, + ], + [[SC::class."::tmissing"], + false, null, + false, null, + ], + [[SC::class."->tmissing"], + false, null, + false, null, + ], + # tableaux avec deux scalaires + [[null, "tsimple"], + false, null, + false, null, + ], + [[null, 'nulib\php\impl\ntsimple'], + false, null, + false, null, + ], + [[null, "tmissing"], + false, null, + false, null, + ], + [[null, "::tstatic"], + false, null, + false, null, + ], + [[null, "->tmethod"], + false, null, + false, null, + ], + [[null, "::tmissing"], + false, null, + false, null, + ], + [[null, "->tmissing"], + false, null, + false, null, + ], + [[false, "tsimple"], + true, [false, "tsimple"], + true, [false, "tsimple"], + ], + [[false, 'nulib\php\impl\ntsimple'], + true, [false, 'nulib\php\impl\ntsimple'], + true, [false, 'nulib\php\impl\ntsimple'], + ], + [[false, "tmissing"], + false, null, + true, [false, "tmissing"], + ], + [[false, "::tstatic"], + false, null, + false, null, + ], + [[false, "->tmethod"], + false, null, + false, null, + ], + [[false, "::tmissing"], + false, null, + false, null, + ], + [[false, "->tmissing"], + false, null, + false, null, + ], + [["", "tsimple"], + false, null, + false, null, + ], + [["", 'nulib\php\impl\ntsimple'], + false, null, + false, null, + ], + [["", "tmissing"], + false, null, + false, null, + ], + [["", "::tstatic"], + false, null, + false, null, + ], + [["", "->tmethod"], + false, null, + false, null, + ], + [["", "::tmissing"], + false, null, + false, null, + ], + [["", "->tmissing"], + false, null, + false, null, + ], + [["xxx", "tmissing"], + false, null, + false, null, + ], + [["xxx", "::tmissing"], + false, null, + false, null, + ], + [["xxx", "->tmissing"], + false, null, + false, null, + ], + [[SC::class, "tstatic"], + false, null, + false, null, + ], + [[SC::class, "::tstatic"], + false, null, + false, null, + ], + [[SC::class, "tmethod"], + false, null, + false, null, + ], + [[SC::class, "->tmethod"], + false, null, + false, null, + ], + [[SC::class, "tmissing"], + false, null, + false, null, + ], + [[SC::class, "::tmissing"], + false, null, + false, null, + ], + [[SC::class, "->tmissing"], + false, null, + false, null, + ], + ]; - self::assertTrue(func::is_static("::xxx")); - self::assertTrue(func::is_static(["xxx"])); - self::assertTrue(func::is_static([null, "yyy"])); - self::assertTrue(func::is_static(["xxx", "yyy"])); - self::assertTrue(func::is_static([null, "yyy", "aaa"])); - self::assertTrue(func::is_static(["xxx", "yyy", "aaa"])); + function testFunction() { + foreach (self::FUNCTION_TESTS as $args) { + [$func, + $verifix1, $func1, + $verifix2, $func2, + ] = $args; + if ($func === ["", "tsimple"]) { + //echo "breakpoint"; + } + + $workf = $func; + $msg = var_export($func, true)." (strict)"; + self::assertSame($verifix1, func::verifix_function($workf, true), "$msg --> verifix"); + if ($verifix1) { + self::assertSame($func1, $workf, "$msg --> func"); + } + + $workf = $func; + $msg = var_export($func, true)." (lenient)"; + self::assertSame($verifix2, func::verifix_function($workf, false), "$msg --> verifix"); + if ($verifix2) { + self::assertSame($func2, $workf, "$msg --> func"); + } + } + } + + const STATIC_TESTS = [ + # scalaires + [null, + false, null, null, + false, null, null, + ], + [false, + false, null, null, + false, null, null, + ], + ["", + false, null, null, + false, null, null, + ], + ["::", + false, null, null, + false, null, null, + ], + ["->", + false, null, null, + false, null, null, + ], + ["tsimple", + false, null, null, + false, null, null, + ], + ['nulib\php\impl\ntsimple', + false, null, null, + false, null, null, + ], + ['tmissing', + false, null, null, + false, null, null, + ], + ["::tstatic", + true, false, [null, "tstatic"], + true, false, [null, "tstatic"], + ], + ["->tmethod", + false, null, null, + false, null, null, + ], + ["::tmissing", + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + ["->tmissing", + false, null, null, + false, null, null, + ], + ["xxx::tmissing", + false, null, null, + true, true, ["xxx", "tmissing"], + ], + ["xxx->tmissing", + false, null, null, + false, null, null, + ], + [SC::class."::tstatic", + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [SC::class."->tmethod", + false, null, null, + false, null, null, + ], + [SC::class."::tmissing", + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [SC::class."->tmissing", + false, null, null, + false, null, null, + ], + # tableaux avec un seul scalaire + [[], + false, null, null, + false, null, null, + ], + [[null], + false, null, null, + false, null, null, + ], + [[false], + false, null, null, + false, null, null, + ], + [[""], + false, null, null, + false, null, null, + ], + [["::"], + false, null, null, + false, null, null, + ], + [["->"], + false, null, null, + false, null, null, + ], + [["tsimple"], + false, null, null, + false, null, null, + ], + [['nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [["::tstatic"], + true, false, [null, "tstatic"], + true, false, [null, "tstatic"], + ], + [["->tmethod"], + false, null, null, + false, null, null, + ], + [["::tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [["->tmissing"], + false, null, null, + false, null, null, + ], + [["xxx::tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [["xxx->tmissing"], + false, null, null, + false, null, null, + ], + [[SC::class."::tstatic"], + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [[SC::class."->tmethod"], + false, null, null, + false, null, null, + ], + [[SC::class."::tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [[SC::class."->tmissing"], + false, null, null, + false, null, null, + ], + # tableaux avec deux scalaires + [[null, "tsimple"], + true, false, [null, "tsimple"], + true, false, [null, "tsimple"], + ], + [[null, 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [[null, "tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [[null, "::tstatic"], + true, false, [null, "tstatic"], + true, false, [null, "tstatic"], + ], + [[null, "->tmethod"], + false, null, null, + false, null, null, + ], + [[null, "::tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [[null, "->tmissing"], + false, null, null, + false, null, null, + ], + [[false, "tsimple"], + false, null, null, + false, null, null, + ], + [[false, 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [[false, "tmissing"], + false, null, null, + false, null, null, + ], + [[false, "::tstatic"], + false, null, null, + false, null, null, + ], + [[false, "->tmethod"], + false, null, null, + false, null, null, + ], + [[false, "::tmissing"], + false, null, null, + false, null, null, + ], + [[false, "->tmissing"], + false, null, null, + false, null, null, + ], + [["", "tsimple"], + false, null, null, + false, null, null, + ], + [["", 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [["", "tmissing"], + false, null, null, + false, null, null, + ], + [["", "::tstatic"], + false, null, null, + false, null, null, + ], + [["", "->tmethod"], + false, null, null, + false, null, null, + ], + [["", "::tmissing"], + false, null, null, + false, null, null, + ], + [["", "->tmissing"], + false, null, null, + false, null, null, + ], + [["xxx", "tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [["xxx", "::tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [["xxx", "->tmissing"], + false, null, null, + false, null, null, + ], + [[SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [[SC::class, "::tstatic"], + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [[SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [[SC::class, "->tmethod"], + false, null, null, + false, null, null, + ], + [[SC::class, "tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [[SC::class, "::tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [[SC::class, "->tmissing"], + false, null, null, + false, null, null, + ], + ]; + + function testStatic() { + foreach (self::STATIC_TESTS as $args) { + [$func, + $verifix1, $bound1, $func1, + $verifix2, $bound2, $func2, + ] = $args; + if ($func === ["", "tsimple"]) { + //echo "breakpoint"; + } + + $workf = $func; + $msg = var_export($func, true)." (strict)"; + self::assertSame($verifix1, func::verifix_static($workf, true, $bound), "$msg --> verifix"); + if ($verifix1) { + self::assertSame($bound1, $bound, "$msg --> bound"); + self::assertSame($func1, $workf, "$msg --> func"); + } + + $workf = $func; + $msg = var_export($func, true)." (lenient)"; + self::assertSame($verifix2, func::verifix_static($workf, false, $bound), "$msg --> verifix"); + if ($verifix2) { + self::assertSame($bound2, $bound, "$msg --> bound"); + self::assertSame($func2, $workf, "$msg --> func"); + } + } + } + + const METHOD_TESTS = [ + # scalaires + [null, + false, null, null, + false, null, null, + ], + [false, + false, null, null, + false, null, null, + ], + ["", + false, null, null, + false, null, null, + ], + ["::", + false, null, null, + false, null, null, + ], + ["->", + false, null, null, + false, null, null, + ], + ["tsimple", + false, null, null, + false, null, null, + ], + ['nulib\php\impl\ntsimple', + false, null, null, + false, null, null, + ], + ['tmissing', + false, null, null, + false, null, null, + ], + ["::tstatic", + false, null, null, + false, null, null, + ], + ["->tmethod", + true, false, [null, "tmethod"], + true, false, [null, "tmethod"], + ], + ["::tmissing", + false, null, null, + false, null, null, + ], + ["->tmissing", + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + ["xxx::tmissing", + false, null, null, + false, null, null, + ], + ["xxx->tmissing", + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [SC::class."::tstatic", + false, null, null, + false, null, null, + ], + [SC::class."->tmethod", + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [SC::class."::tmissing", + false, null, null, + false, null, null, + ], + [SC::class."->tmissing", + false, null, null, + true, true, [SC::class, "tmissing"], + ], + # tableaux avec un seul scalaire + [[], + false, null, null, + false, null, null, + ], + [[null], + false, null, null, + false, null, null, + ], + [[false], + false, null, null, + false, null, null, + ], + [[""], + false, null, null, + false, null, null, + ], + [["::"], + false, null, null, + false, null, null, + ], + [["->"], + false, null, null, + false, null, null, + ], + [["tsimple"], + false, null, null, + false, null, null, + ], + [['nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [["::tstatic"], + false, null, null, + false, null, null, + ], + [["->tmethod"], + true, false, [null, "tmethod"], + true, false, [null, "tmethod"], + ], + [["::tmissing"], + false, null, null, + false, null, null, + ], + [["->tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [["xxx::tmissing"], + false, null, null, + false, null, null, + ], + [["xxx->tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [[SC::class."::tstatic"], + false, null, null, + false, null, null, + ], + [[SC::class."->tmethod"], + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [[SC::class."::tmissing"], + false, null, null, + false, null, null, + ], + [[SC::class."->tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + # tableaux avec deux scalaires + [[null, "tsimple"], + true, false, [null, "tsimple"], + true, false, [null, "tsimple"], + ], + [[null, 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [[null, "tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [[null, "::tstatic"], + false, null, null, + false, null, null, + ], + [[null, "->tmethod"], + true, false, [null, "tmethod"], + true, false, [null, "tmethod"], + ], + [[null, "::tmissing"], + false, null, null, + false, null, null, + ], + [[null, "->tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [[false, "tsimple"], + false, null, null, + false, null, null, + ], + [[false, 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [[false, "tmissing"], + false, null, null, + false, null, null, + ], + [[false, "::tstatic"], + false, null, null, + false, null, null, + ], + [[false, "->tmethod"], + false, null, null, + false, null, null, + ], + [[false, "::tmissing"], + false, null, null, + false, null, null, + ], + [[false, "->tmissing"], + false, null, null, + false, null, null, + ], + [["", "tsimple"], + false, null, null, + false, null, null, + ], + [["", 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [["", "tmissing"], + false, null, null, + false, null, null, + ], + [["", "::tstatic"], + false, null, null, + false, null, null, + ], + [["", "->tmethod"], + false, null, null, + false, null, null, + ], + [["", "::tmissing"], + false, null, null, + false, null, null, + ], + [["", "->tmissing"], + false, null, null, + false, null, null, + ], + [["xxx", "tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [["xxx", "::tmissing"], + false, null, null, + false, null, null, + ], + [["xxx", "->tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [[SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [[SC::class, "::tstatic"], + false, null, null, + false, null, null, + ], + [[SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [[SC::class, "->tmethod"], + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [[SC::class, "tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [[SC::class, "::tmissing"], + false, null, null, + false, null, null, + ], + [[SC::class, "->tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + ]; + + function testMethod() { + foreach (self::METHOD_TESTS as $args) { + [$func, + $verifix1, $bound1, $func1, + $verifix2, $bound2, $func2, + ] = $args; + + $workf = $func; + $msg = var_export($func, true)." (strict)"; + self::assertSame($verifix1, func::verifix_method($workf, true, $bound), "$msg --> verifix"); + if ($verifix1) { + self::assertSame($bound1, $bound, "$msg --> bound"); + self::assertSame($func1, $workf, "$msg --> func"); + } + + $workf = $func; + $msg = var_export($func, true)." (lenient)"; + self::assertSame($verifix2, func::verifix_method($workf, false, $bound), "$msg --> verifix"); + if ($verifix2) { + self::assertSame($bound2, $bound, "$msg --> bound"); + self::assertSame($func2, $workf, "$msg --> func"); + } + } + } + + function testInvokeFunction() { + # m1 + self::assertSame([null], func::call("tm1")); + self::assertSame([null], func::call("tm1", null)); + self::assertSame([null], func::call("tm1", null, null)); + self::assertSame([null], func::call("tm1", null, null, null)); + self::assertSame([null], func::call("tm1", null, null, null, null)); + self::assertSame([1], func::call("tm1", 1)); + self::assertSame([1], func::call("tm1", 1, 2)); + self::assertSame([1], func::call("tm1", 1, 2, 3)); + self::assertSame([1], func::call("tm1", 1, 2, 3, 4)); + + # o1 + self::assertSame([9], func::call("to1")); + self::assertSame([null], func::call("to1", null)); + self::assertSame([null], func::call("to1", null, null)); + self::assertSame([null], func::call("to1", null, null, null)); + self::assertSame([null], func::call("to1", null, null, null, null)); + self::assertSame([1], func::call("to1", 1)); + self::assertSame([1], func::call("to1", 1, 2)); + self::assertSame([1], func::call("to1", 1, 2, 3)); + self::assertSame([1], func::call("to1", 1, 2, 3, 4)); + + # v + self::assertSame([], func::call("tv")); + self::assertSame([null], func::call("tv", null)); + self::assertSame([null, null], func::call("tv", null, null)); + self::assertSame([null, null, null], func::call("tv", null, null, null)); + self::assertSame([null, null, null, null], func::call("tv", null, null, null, null)); + self::assertSame([1], func::call("tv", 1)); + self::assertSame([1, 2], func::call("tv", 1, 2)); + self::assertSame([1, 2, 3], func::call("tv", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], func::call("tv", 1, 2, 3, 4)); + + # m1o1 + self::assertSame([null, 9], func::call("tm1o1")); + self::assertSame([null, 9], func::call("tm1o1", null)); + self::assertSame([null, null], func::call("tm1o1", null, null)); + self::assertSame([null, null], func::call("tm1o1", null, null, null)); + self::assertSame([null, null], func::call("tm1o1", null, null, null, null)); + self::assertSame([1, 9], func::call("tm1o1", 1)); + self::assertSame([1, 2], func::call("tm1o1", 1, 2)); + self::assertSame([1, 2], func::call("tm1o1", 1, 2, 3)); + self::assertSame([1, 2], func::call("tm1o1", 1, 2, 3, 4)); + + # m1v + self::assertSame([null], func::call("tm1v")); + self::assertSame([null], func::call("tm1v", null)); + self::assertSame([null, null], func::call("tm1v", null, null)); + self::assertSame([null, null, null], func::call("tm1v", null, null, null)); + self::assertSame([null, null, null, null], func::call("tm1v", null, null, null, null)); + self::assertSame([1], func::call("tm1v", 1)); + self::assertSame([1, 2], func::call("tm1v", 1, 2)); + self::assertSame([1, 2, 3], func::call("tm1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], func::call("tm1v", 1, 2, 3, 4)); + + # m1o1v + self::assertSame([null, 9], func::call("tm1o1v")); + self::assertSame([null, 9], func::call("tm1o1v", null)); + self::assertSame([null, null], func::call("tm1o1v", null, null)); + self::assertSame([null, null, null], func::call("tm1o1v", null, null, null)); + self::assertSame([null, null, null, null], func::call("tm1o1v", null, null, null, null)); + self::assertSame([1, 9], func::call("tm1o1v", 1)); + self::assertSame([1, 2], func::call("tm1o1v", 1, 2)); + self::assertSame([1, 2, 3], func::call("tm1o1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], func::call("tm1o1v", 1, 2, 3, 4)); + + # o1v + self::assertSame([9], func::call("to1v")); + self::assertSame([null], func::call("to1v", null)); + self::assertSame([null, null], func::call("to1v", null, null)); + self::assertSame([null, null, null], func::call("to1v", null, null, null)); + self::assertSame([null, null, null, null], func::call("to1v", null, null, null, null)); + self::assertSame([1], func::call("to1v", 1)); + self::assertSame([1, 2], func::call("to1v", 1, 2)); + self::assertSame([1, 2, 3], func::call("to1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], func::call("to1v", 1, 2, 3, 4)); + } + + function testInvokeClass() { + $func = func::with(SC::class); + self::assertInstanceOf(SC::class, $func->invoke()); + self::assertInstanceOf(SC::class, $func->invoke([])); + self::assertInstanceOf(SC::class, $func->invoke([1])); + self::assertInstanceOf(SC::class, $func->invoke([1, 2])); + self::assertInstanceOf(SC::class, $func->invoke([1, 2, 3])); + + $func = func::with(C0::class); + self::assertInstanceOf(C0::class, $func->invoke()); + self::assertInstanceOf(C0::class, $func->invoke([])); + self::assertInstanceOf(C0::class, $func->invoke([1])); + self::assertInstanceOf(C0::class, $func->invoke([1, 2])); + self::assertInstanceOf(C0::class, $func->invoke([1, 2, 3])); + + $func = func::with(C1::class); + /** @var C1 $i1 */ + $i1 = $func->invoke(); + self::assertInstanceOf(C1::class, $i1); self::assertSame(0, $i1->base); + $i1 = $func->invoke([]); + self::assertInstanceOf(C1::class, $i1); self::assertSame(0, $i1->base); + $i1 = $func->invoke([1]); + self::assertInstanceOf(C1::class, $i1); self::assertSame(1, $i1->base); + $i1 = $func->invoke([1, 2]); + self::assertInstanceOf(C1::class, $i1); self::assertSame(1, $i1->base); } - function testFix_static() { - $class = "class"; - $func = "::xxx"; - func::fix_static($func, $class); - self::assertSame("class::xxx", $func); - $func = ["xxx"]; - func::fix_static($func, $class); - self::assertSame(["class", "xxx"], $func); - $func = [null, "yyy"]; - func::fix_static($func, $class); - self::assertSame(["class", "yyy"], $func); - $func = ["xxx", "yyy"]; - func::fix_static($func, $class); - self::assertSame(["xxx", "yyy"], $func); - $func = [null, "yyy", "aaa"]; - func::fix_static($func, $class); - self::assertSame(["class", "yyy", "aaa"], $func); - $func = ["xxx", "yyy", "aaa"]; - func::fix_static($func, $class); - self::assertSame(["xxx", "yyy", "aaa"], $func); - } - - function testIs_method() { - self::assertFalse(func::is_method(null)); - self::assertFalse(func::is_method("")); - self::assertFalse(func::is_method("->")); - self::assertFalse(func::is_method([])); - self::assertFalse(func::is_method([""])); - self::assertFalse(func::is_method([null, "->"])); - self::assertFalse(func::is_method(["xxx", "->"])); - - self::assertTrue(func::is_method("->xxx")); - self::assertTrue(func::is_method(["->xxx"])); - self::assertTrue(func::is_method([null, "->yyy"])); - self::assertTrue(func::is_method(["xxx", "->yyy"])); - self::assertTrue(func::is_method([null, "->yyy", "aaa"])); - self::assertTrue(func::is_method(["xxx", "->yyy", "aaa"])); - } - - function testFix_method() { - $object = new \stdClass(); - $func= "->xxx"; - func::fix_method($func, $object); - self::assertSame([$object, "xxx"], $func); - $func= ["->xxx"]; - func::fix_method($func, $object); - self::assertSame([$object, "xxx"], $func); - $func= [null, "->yyy"]; - func::fix_method($func, $object); - self::assertSame([$object, "yyy"], $func); - $func= ["xxx", "->yyy"]; - func::fix_method($func, $object); - self::assertSame([$object, "yyy"], $func); - $func= [null, "->yyy", "aaa"]; - func::fix_method($func, $object); - self::assertSame([$object, "yyy", "aaa"], $func); - $func= ["xxx", "->yyy", "aaa"]; - func::fix_method($func, $object); - self::assertSame([$object, "yyy", "aaa"], $func); - } - - function testCall() { - self::assertSame(36, func::call("func36")); - self::assertSame(12, func::call(TC::class."::method")); - self::assertSame(12, func::call([TC::class, "method"])); - $closure = function() { - return 21; + private static function invoke_asserts(): array { + $inv_ok = function($func) { + return func::with($func)->invoke(); }; - self::assertSame(21, func::call($closure)); + $inv_ko = function($func) use ($inv_ok) { + return function() use ($func, $inv_ok) { + return $inv_ok($func); + }; + }; + $bind_ok = function($func, $objet) { + return func::with($func)->bind($objet)->invoke(); + }; + $bind_ko = function($func, $object) use ($bind_ok) { + return function() use ($func, $object, $bind_ok) { + return $bind_ok($func, $object); + }; + }; + return [$inv_ok, $inv_ko, $bind_ok, $bind_ko]; } - 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], func::call("func_m1")); - self::assertSame([null], func::call("func_m1", null)); - self::assertSame([null], func::call("func_m1", null, null)); - self::assertSame([null], func::call("func_m1", null, null, null)); - self::assertSame([null], func::call("func_m1", null, null, null, null)); - self::assertSame([1], func::call("func_m1", 1)); - self::assertSame([1], func::call("func_m1", 1, 2)); - self::assertSame([1], func::call("func_m1", 1, 2, 3)); - self::assertSame([1], func::call("func_m1", 1, 2, 3, 4)); - - # o1 - self::assertSame([9], func::call("func_o1")); - self::assertSame([null], func::call("func_o1", null)); - self::assertSame([null], func::call("func_o1", null, null)); - self::assertSame([null], func::call("func_o1", null, null, null)); - self::assertSame([null], func::call("func_o1", null, null, null, null)); - self::assertSame([1], func::call("func_o1", 1)); - self::assertSame([1], func::call("func_o1", 1, 2)); - self::assertSame([1], func::call("func_o1", 1, 2, 3)); - self::assertSame([1], func::call("func_o1", 1, 2, 3, 4)); - - # v - self::assertSame([], func::call("func_v")); - self::assertSame([null], func::call("func_v", null)); - self::assertSame([null, null], func::call("func_v", null, null)); - self::assertSame([null, null, null], func::call("func_v", null, null, null)); - self::assertSame([null, null, null, null], func::call("func_v", null, null, null, null)); - self::assertSame([1], func::call("func_v", 1)); - self::assertSame([1, 2], func::call("func_v", 1, 2)); - self::assertSame([1, 2, 3], func::call("func_v", 1, 2, 3)); - self::assertSame([1, 2, 3, 4], func::call("func_v", 1, 2, 3, 4)); - - # m1o1 - self::assertSame([null, 9], func::call("func_m1o1")); - self::assertSame([null, 9], func::call("func_m1o1", null)); - self::assertSame([null, null], func::call("func_m1o1", null, null)); - self::assertSame([null, null], func::call("func_m1o1", null, null, null)); - self::assertSame([null, null], func::call("func_m1o1", null, null, null, null)); - self::assertSame([1, 9], func::call("func_m1o1", 1)); - self::assertSame([1, 2], func::call("func_m1o1", 1, 2)); - self::assertSame([1, 2], func::call("func_m1o1", 1, 2, 3)); - self::assertSame([1, 2], func::call("func_m1o1", 1, 2, 3, 4)); - - # m1v - self::assertSame([null], func::call("func_m1v")); - self::assertSame([null], func::call("func_m1v", null)); - self::assertSame([null, null], func::call("func_m1v", null, null)); - self::assertSame([null, null, null], func::call("func_m1v", null, null, null)); - self::assertSame([null, null, null, null], func::call("func_m1v", null, null, null, null)); - self::assertSame([1], func::call("func_m1v", 1)); - self::assertSame([1, 2], func::call("func_m1v", 1, 2)); - self::assertSame([1, 2, 3], func::call("func_m1v", 1, 2, 3)); - self::assertSame([1, 2, 3, 4], func::call("func_m1v", 1, 2, 3, 4)); - - # m1o1v - self::assertSame([null, 9], func::call("func_m1o1v")); - self::assertSame([null, 9], func::call("func_m1o1v", null)); - self::assertSame([null, null], func::call("func_m1o1v", null, null)); - self::assertSame([null, null, null], func::call("func_m1o1v", null, null, null)); - self::assertSame([null, null, null, null], func::call("func_m1o1v", null, null, null, null)); - self::assertSame([1, 9], func::call("func_m1o1v", 1)); - self::assertSame([1, 2], func::call("func_m1o1v", 1, 2)); - self::assertSame([1, 2, 3], func::call("func_m1o1v", 1, 2, 3)); - self::assertSame([1, 2, 3, 4], func::call("func_m1o1v", 1, 2, 3, 4)); - - # o1v - self::assertSame([9], func::call("func_o1v")); - self::assertSame([null], func::call("func_o1v", null)); - self::assertSame([null, null], func::call("func_o1v", null, null)); - self::assertSame([null, null, null], func::call("func_o1v", null, null, null)); - self::assertSame([null, null, null, null], func::call("func_o1v", null, null, null, null)); - self::assertSame([1], func::call("func_o1v", 1)); - self::assertSame([1, 2], func::call("func_o1v", 1, 2)); - self::assertSame([1, 2, 3], func::call("func_o1v", 1, 2, 3)); - self::assertSame([1, 2, 3, 4], func::call("func_o1v", 1, 2, 3, 4)); + function testInvokeStatic() { + [$inv_ok, $inv_ko, $bind_ok, $bind_ko] = self::invoke_asserts(); + $sc = new SC(); + + self::assertSame(10, $inv_ok([SC::class, "tstatic"])); + self::assertSame(10, $inv_ok([SC::class, "::tstatic"])); + self::assertSame(10, $inv_ok([SC::class, "->tstatic"])); + + self::assertSame(10, $inv_ok([$sc, "tstatic"])); + self::assertSame(10, $inv_ok([$sc, "::tstatic"])); + self::assertSame(10, $inv_ok([$sc, "->tstatic"])); + + self::assertException(ValueException::class, $inv_ko([null, "tstatic"])); + self::assertException(ValueException::class, $inv_ko([null, "::tstatic"])); + self::assertException(ValueException::class, $inv_ko([null, "->tstatic"])); + + self::assertSame(10, $bind_ok([null, "tstatic"], SC::class)); + self::assertSame(10, $bind_ok([null, "::tstatic"], SC::class)); + self::assertSame(10, $bind_ok([null, "->tstatic"], SC::class)); + + self::assertSame(10, $bind_ok([null, "tstatic"], $sc)); + self::assertSame(10, $bind_ok([null, "::tstatic"], $sc)); + self::assertSame(10, $bind_ok([null, "->tstatic"], $sc)); } - function testCall_all() { - $c1 = new C1(); - $c2 = new C2(); - $c3 = new C3(); + function testInvokeMethod() { + [$inv_ok, $inv_ko, $bind_ok, $bind_ko] = self::invoke_asserts(); + $sc = new SC(); - self::assertSameValues([11, 12], func::call_all(C1::class)); - self::assertSameValues([11, 12, 21, 22], func::call_all($c1)); - self::assertSameValues([13, 11, 12], func::call_all(C2::class)); - self::assertSameValues([13, 23, 11, 12, 21, 22], func::call_all($c2)); - self::assertSameValues([111, 13, 12], func::call_all(C3::class)); - self::assertSameValues([111, 121, 13, 23, 12, 22], func::call_all($c3)); + self::assertException(ReflectionException::class, $inv_ko([SC::class, "tmethod"])); + self::assertException(ReflectionException::class, $inv_ko([SC::class, "::tmethod"])); + self::assertException(ReflectionException::class, $inv_ko([SC::class, "->tmethod"])); - $options = "conf"; - self::assertSameValues([11], func::call_all(C1::class, $options)); - self::assertSameValues([11, 21], func::call_all($c1, $options)); - self::assertSameValues([11], func::call_all(C2::class, $options)); - self::assertSameValues([11, 21], func::call_all($c2, $options)); - self::assertSameValues([111], func::call_all(C3::class, $options)); - self::assertSameValues([111, 121], func::call_all($c3, $options)); + self::assertSame(11, $inv_ok([$sc, "tmethod"])); + self::assertException(ReflectionException::class, $inv_ko([$sc, "::tmethod"])); + self::assertSame(11, $inv_ok([$sc, "->tmethod"])); - $options = ["prefix" => "conf"]; - self::assertSameValues([11], func::call_all(C1::class, $options)); - self::assertSameValues([11, 21], func::call_all($c1, $options)); - self::assertSameValues([11], func::call_all(C2::class, $options)); - self::assertSameValues([11, 21], func::call_all($c2, $options)); - self::assertSameValues([111], func::call_all(C3::class, $options)); - self::assertSameValues([111, 121], func::call_all($c3, $options)); + self::assertException(ValueException::class, $inv_ko([null, "tmethod"])); + self::assertException(ValueException::class, $inv_ko([null, "::tmethod"])); + self::assertException(ValueException::class, $inv_ko([null, "->tmethod"])); - self::assertSameValues([11, 12], func::call_all($c1, ["include" => "x"])); - self::assertSameValues([11, 21], func::call_all($c1, ["include" => "y"])); - self::assertSameValues([11, 12, 21], func::call_all($c1, ["include" => ["x", "y"]])); + self::assertException(ReflectionException::class, $bind_ko([null, "tmethod"], SC::class)); + self::assertException(ReflectionException::class, $bind_ko([null, "::tmethod"], SC::class)); + self::assertException(ReflectionException::class, $bind_ko([null, "->tmethod"], SC::class)); - self::assertSameValues([21, 22], func::call_all($c1, ["exclude" => "x"])); - self::assertSameValues([12, 22], func::call_all($c1, ["exclude" => "y"])); - self::assertSameValues([22], func::call_all($c1, ["exclude" => ["x", "y"]])); - - self::assertSameValues([12], func::call_all($c1, ["include" => "x", "exclude" => "y"])); + self::assertSame(11, $bind_ok([null, "tmethod"], $sc)); + self::assertException(ReflectionException::class, $bind_ko([null, "::tmethod"], $sc)); + self::assertSame(11, $bind_ok([null, "->tmethod"], $sc)); } - function testCons() { - $obj1 = func::cons(WoCons::class, 1, 2, 3); - self::assertInstanceOf(WoCons::class, $obj1); + function testArgs() { + $func = function(int $a, int $b, int $c): int { + return $a + $b + $c; + }; - $obj2 = func::cons(WithEmptyCons::class, 1, 2, 3); - self::assertInstanceOf(WithEmptyCons::class, $obj2); + self::assertSame(6, func::call($func, 1, 2, 3)); + self::assertSame(6, func::call($func, 1, 2, 3, 4)); - $obj3 = func::cons(WithCons::class, 1, 2, 3); - self::assertInstanceOf(WithCons::class, $obj3); - self::assertSame(1, $obj3->first); + self::assertSame(6, func::with($func)->invoke([1, 2, 3])); + self::assertSame(6, func::with($func, [1])->invoke([2, 3])); + self::assertSame(6, func::with($func, [1, 2])->invoke([3])); + self::assertSame(6, func::with($func, [1, 2, 3])->invoke()); + self::assertSame(6, func::with($func, [1, 2, 3, 4])->invoke()); + } + + function testRebind() { + $func = func::with([C1::class, "tmethod"]); + self::assertSame(11, $func->bind(new C1(0))->invoke()); + self::assertSame(12, $func->bind(new C1(1))->invoke()); + self::assertException(ValueException::class, function() use ($func) { + $func->bind(new C0())->invoke(); + }); + } + } +} + +namespace { + function tsimple(): int { return 0; } + function tm1($a): array { return [$a]; } + function to1($b=9): array { return [$b]; } + function tv(...$c): array { return [...$c]; } + function tm1o1($a, $b=9): array { return [$a, $b]; } + function tm1v($a, ...$c): array { return [$a, ...$c]; } + function tm1o1v($a, $b=9, ...$c): array { return [$a, $b, ...$c]; } + function to1v($b=9, ...$c): array { return [$b, ...$c]; } +} + +namespace nulib\php\impl { + function ntsimple(): int { return 0; } + + class SC { + static function tstatic(): int { + return 10; + } + + function tmethod(): int { + return 11; } } - class WoCons { - } - class WithEmptyCons { + class C0 { function __construct() { } - } - class WithCons { - public $first; - function __construct($first) { - $this->first = $first; - } - } - class TC { - static function method() { - return 12; + static function tstatic(): int { + return 10; + } + + function tmethod(): int { + return 11; } } class C1 { - static function confps1_xy() { - return 11; + function __construct(int $base=0) { + $this->base = $base; } - static function ps2_x() { - return 12; + + public int $base; + + static function tstatic(): int { + return 10; } - 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; + + function tmethod(): int { + return 11 + $this->base; } } } diff --git a/php/tests/php/nur_funcTest.php b/php/tests/php/nur_funcTest.php new file mode 100644 index 0000000..44fa744 --- /dev/null +++ b/php/tests/php/nur_funcTest.php @@ -0,0 +1,292 @@ +")); + 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; + } + } +} diff --git a/php/tests/schema/_scalar/ScalarSchemaTest.php b/php/tests/schema/_scalar/ScalarSchemaTest.php index 67edc35..e004168 100644 --- a/php/tests/schema/_scalar/ScalarSchemaTest.php +++ b/php/tests/schema/_scalar/ScalarSchemaTest.php @@ -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 = [ diff --git a/php/tests/schema/types/strTest.php b/php/tests/schema/types/strTest.php index 78e33c9..45eda17 100644 --- a/php/tests/schema/types/strTest.php +++ b/php/tests/schema/types/strTest.php @@ -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 {