diff --git a/.idea/nulib-base.iml b/.idea/nulib-base.iml
index c88c3c8..419ada4 100644
--- a/.idea/nulib-base.iml
+++ b/.idea/nulib-base.iml
@@ -4,6 +4,7 @@
+
diff --git a/bin/._pman-composer_local_deps.php b/bin/._pman-composer_local_deps.php
index 92aeda8..c99d615 100755
--- a/bin/._pman-composer_local_deps.php
+++ b/bin/._pman-composer_local_deps.php
@@ -2,9 +2,7 @@
|<|>|<=|>=|(?:is\s+)?null|(?:is\s+)?not\s+null)\s*(.*)$/', $arg, $ms);
+ }
+
+ protected function storageCtl(CapacitorStorage $storage): void {
+ $args = $this->args;
+
+ $channelClass = $this->channelClass;
+ $tableName = $this->tableName;
+ if ($channelClass === null && $tableName === null) {
+ $name = A::shift($args);
+ if ($name !== null) {
+ if (!$storage->channelExists($name, $row)) {
+ self::die("$name: nom de canal de données introuvable");
+ }
+ if ($row["class_name"] !== "class@anonymous") $channelClass = $row["class_name"];
+ else $tableName = $row["table_name"];
+ }
+ }
+ if ($channelClass !== null) {
+ $channelClass = str_replace("/", "\\", $channelClass);
+ $channel = new $channelClass;
+ } elseif ($tableName !== null) {
+ $channel = new class($tableName) extends CapacitorChannel {
+ function __construct(?string $name=null) {
+ parent::__construct($name);
+ $this->tableName = $name;
+ }
+ };
+ } else {
+ $found = false;
+ foreach ($storage->getChannels() as $row) {
+ msg::print($row["name"]);
+ $found = true;
+ }
+ if ($found) self::exit();
+ self::die("Vous devez spécifier le canal de données");
+ }
+ $capacitor = new Capacitor($storage, $channel);
+
+ switch ($this->action) {
+ case self::ACTION_RESET:
+ $capacitor->reset($this->recreate);
+ break;
+ case self::ACTION_QUERY:
+ if (!$args) {
+ # lister les id
+ $out = new Stream(STDOUT);
+ $primaryKeys = $storage->getPrimaryKeys($channel);
+ $rows = $storage->db()->all([
+ "select",
+ "cols" => $primaryKeys,
+ "from" => $channel->getTableName(),
+ ]);
+ $out->fputcsv($primaryKeys);
+ foreach ($rows as $row) {
+ $rowIds = $storage->getRowIds($channel, $row);
+ $out->fputcsv($rowIds);
+ }
+ } else {
+ # afficher les lignes correspondantes
+ if (count($args) == 1 && !self::isa_cond($args[0])) {
+ $filter = $args[0];
+ } else {
+ $filter = [];
+ $ms = null;
+ foreach ($args as $arg) {
+ if (self::isa_cond($arg, $ms)) {
+ $filter[$ms[1]] = [$ms[2], $ms[3]];
+ } else {
+ $filter[$arg] = ["not null"];
+ }
+ }
+ }
+ $first = true;
+ $capacitor->each($filter, function ($row) use (&$first) {
+ if ($first) $first = false;
+ else echo "---\n";
+ yaml::dump($row);
+ });
+ }
+ break;
+ case self::ACTION_SQL:
+ echo $capacitor->getCreateSql()."\n";
+ break;
+ }
+ }
+}
diff --git a/php/cli/BgLauncherApp.php b/php/cli/BgLauncherApp.php
new file mode 100644
index 0000000..3f965fd
--- /dev/null
+++ b/php/cli/BgLauncherApp.php
@@ -0,0 +1,122 @@
+ "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;
+
+ 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/cli/DumpserApp.php b/php/cli/DumpserApp.php
new file mode 100644
index 0000000..61c4aa7
--- /dev/null
+++ b/php/cli/DumpserApp.php
@@ -0,0 +1,31 @@
+ parent::ARGS,
+ "purpose" => "afficher des données sérialisées",
+ ];
+
+ function main() {
+ $files = [];
+ foreach ($this->args as $arg) {
+ if (is_file($arg)) {
+ $files[] = $arg;
+ } else {
+ msg::warning("$arg: fichier invalide ou introuvable");
+ }
+ }
+ $showSection = count($files) > 1;
+ foreach ($files as $file) {
+ if ($showSection) msg::section($file);
+ $sfile = new SharedFile($file);
+ yaml::dump($sfile->unserialize());
+ }
+ }
+}
diff --git a/php/cli/Json2yamlApp.php b/php/cli/Json2yamlApp.php
new file mode 100644
index 0000000..138f184
--- /dev/null
+++ b/php/cli/Json2yamlApp.php
@@ -0,0 +1,21 @@
+args[0] ?? null;
+ if ($input === null || $input === "-") {
+ $output = null;
+ } else {
+ $output = path::ensure_ext($input, ".yml", ".json");
+ }
+
+ $data = json::load($input);
+ yaml::dump($data, $output);
+ }
+}
\ No newline at end of file
diff --git a/php/cli/MysqlCapacitorApp.php b/php/cli/MysqlCapacitorApp.php
new file mode 100644
index 0000000..a188782
--- /dev/null
+++ b/php/cli/MysqlCapacitorApp.php
@@ -0,0 +1,45 @@
+ parent::ARGS,
+ "purpose" => "gestion d'un capacitor mysql",
+ "usage" => [
+ "DBCONN [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] [--query] key=value...",
+ "DBCONN [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] --sql-create",
+ ],
+ ["-t", "--table-name", "args" => 1,
+ "help" => "nom de la table porteuse du canal de données",
+ ],
+ ["-c", "--channel-class", "args" => 1,
+ "help" => "nom de la classe dérivée de CapacitorChannel",
+ ],
+ ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET,
+ "help" => "réinitialiser le canal",
+ ],
+ ["-n", "--no-recreate", "name" => "recreate", "value" => false,
+ "help" => "ne pas recréer la table correspondant au canal"
+ ],
+ ["--query", "name" => "action", "value" => self::ACTION_QUERY,
+ "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut",
+ ],
+ ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL,
+ "help" => "afficher la requête pour créer la table",
+ ],
+ ];
+
+ function main() {
+ $dbconn = A::shift($this->args);
+ if ($dbconn === null) self::die("Vous devez spécifier la base de données");
+ $tmp = config::db($dbconn);
+ if ($tmp === null) self::die("$dbconn: base de données invalide");
+ $storage = new MysqlStorage($tmp);
+
+ $this->storageCtl($storage);
+ }
+}
diff --git a/php/cli/PgsqlCapacitorApp.php b/php/cli/PgsqlCapacitorApp.php
new file mode 100644
index 0000000..978ee51
--- /dev/null
+++ b/php/cli/PgsqlCapacitorApp.php
@@ -0,0 +1,45 @@
+ parent::ARGS,
+ "purpose" => "gestion d'un capacitor pgsql",
+ "usage" => [
+ "DBCONN [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] [--query] key=value...",
+ "DBCONN [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] --sql-create",
+ ],
+ ["-t", "--table-name", "args" => 1,
+ "help" => "nom de la table porteuse du canal de données",
+ ],
+ ["-c", "--channel-class", "args" => 1,
+ "help" => "nom de la classe dérivée de CapacitorChannel",
+ ],
+ ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET,
+ "help" => "réinitialiser le canal",
+ ],
+ ["-n", "--no-recreate", "name" => "recreate", "value" => false,
+ "help" => "ne pas recréer la table correspondant au canal"
+ ],
+ ["--query", "name" => "action", "value" => self::ACTION_QUERY,
+ "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut",
+ ],
+ ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL,
+ "help" => "afficher la requête pour créer la table",
+ ],
+ ];
+
+ function main() {
+ $dbconn = A::shift($this->args);
+ if ($dbconn === null) self::die("Vous devez spécifier la base de données");
+ $tmp = config::db($dbconn);
+ if ($tmp === null) self::die("$dbconn: base de données invalide");
+ $storage = new PgsqlStorage($tmp);
+
+ $this->storageCtl($storage);
+ }
+}
diff --git a/php/cli/SqliteCapacitorApp.php b/php/cli/SqliteCapacitorApp.php
new file mode 100644
index 0000000..daef081
--- /dev/null
+++ b/php/cli/SqliteCapacitorApp.php
@@ -0,0 +1,43 @@
+ parent::ARGS,
+ "purpose" => "gestion d'un capacitor sqlite",
+ "usage" => [
+ "DBFILE [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] [--query] key=value...",
+ "DBFILE [CHANNEL_NAME | -t TABLE | -c CHANNEL_CLASS] --sql-create",
+ ],
+ ["-t", "--table-name", "args" => 1,
+ "help" => "nom de la table porteuse du canal de données",
+ ],
+ ["-c", "--channel-class", "args" => 1,
+ "help" => "nom de la classe dérivée de CapacitorChannel",
+ ],
+ ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET,
+ "help" => "réinitialiser le canal",
+ ],
+ ["-n", "--no-recreate", "name" => "recreate", "value" => false,
+ "help" => "ne pas recréer la table correspondant au canal"
+ ],
+ ["--query", "name" => "action", "value" => self::ACTION_QUERY,
+ "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut",
+ ],
+ ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL,
+ "help" => "afficher la requête pour créer la table",
+ ],
+ ];
+
+ function main() {
+ $dbfile = A::shift($this->args);
+ if ($dbfile === null) self::die("Vous devez spécifier la base de données");
+ if (!file_exists($dbfile)) self::die("$dbfile: fichier introuvable");
+ $storage = new SqliteStorage($dbfile);
+
+ $this->storageCtl($storage);
+ }
+}
diff --git a/php/cli/SteamTrainApp.php b/php/cli/SteamTrainApp.php
new file mode 100644
index 0000000..6406cbf
--- /dev/null
+++ b/php/cli/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/cli/Yaml2jsonApp.php b/php/cli/Yaml2jsonApp.php
new file mode 100644
index 0000000..fb3f96f
--- /dev/null
+++ b/php/cli/Yaml2jsonApp.php
@@ -0,0 +1,21 @@
+args[0] ?? null;
+ if ($input === null || $input === "-") {
+ $output = null;
+ } else {
+ $output = path::ensure_ext($input, ".json", [".yml", ".yaml"]);
+ }
+
+ $data = yaml::load($input);
+ json::dump($data, $output);
+ }
+}
\ No newline at end of file
diff --git a/php/src/tools/pman/ComposerFile.php b/php/cli/pman/ComposerFile.php
similarity index 99%
rename from php/src/tools/pman/ComposerFile.php
rename to php/cli/pman/ComposerFile.php
index c3dd7a1..1e72de2 100644
--- a/php/src/tools/pman/ComposerFile.php
+++ b/php/cli/pman/ComposerFile.php
@@ -1,5 +1,5 @@
getParams();
+ } elseif ($app instanceof Application) {
+ $class = get_class($app);
+ $params = [
+ "class" => $class,
+ "projdir" => $app::PROJDIR,
+ "vendor" => $app::VENDOR,
+ "appcode" => $app::APPCODE,
+ "datadir" => $app::DATADIR,
+ "etcdir" => $app::ETCDIR,
+ "vardir" => $app::VARDIR,
+ "logdir" => $app::LOGDIR,
+ "appgroup" => $app::APPGROUP,
+ "name" => $app::NAME,
+ "title" => $app::TITLE,
+ ];
+ } elseif (self::isa_Application($app)) {
+ $class = $app;
+ $params = [
+ "class" => $class,
+ "projdir" => constant("$app::PROJDIR"),
+ "vendor" => constant("$app::VENDOR"),
+ "appcode" => constant("$app::APPCODE"),
+ "datadir" => constant("$app::DATADIR"),
+ "etcdir" => constant("$app::ETCDIR"),
+ "vardir" => constant("$app::VARDIR"),
+ "logdir" => constant("$app::LOGDIR"),
+ "appgroup" => constant("$app::APPGROUP"),
+ "name" => constant("$app::NAME"),
+ "title" => constant("$app::TITLE"),
+ ];
+ } elseif (is_array($app)) {
+ $params = $app;
+ } else {
+ throw ValueException::invalid_type($app, Application::class);
+ }
+ return $params;
+ }
+
+ protected static ?self $app = null;
+
+ /**
+ * @param Application|string|array $app
+ * @param Application|string|array|null $proj
+ */
+ static function with($app, $proj=null): self {
+ $params = self::get_params($app);
+ $proj ??= self::params_getenv();
+ $proj ??= self::$app;
+ $proj_params = $proj !== null? self::get_params($proj): null;
+ if ($proj_params !== null) {
+ A::merge($params, cl::select($proj_params, [
+ "projdir",
+ "vendor",
+ "appcode",
+ "cwd",
+ "datadir",
+ "etcdir",
+ "vardir",
+ "logdir",
+ "profile",
+ "facts",
+ "debug",
+ ]));
+ }
+ return new static($params, $proj_params !== null);
+ }
+
+ static function init($app, $proj=null): void {
+ self::$app = static::with($app, $proj);
+ }
+
+ static function get(): self {
+ return self::$app ??= new static(null);
+ }
+
+ static function params_putenv(): void {
+ $params = serialize(self::get()->getParams());
+ putenv("NULIB_APP_app_params=$params");
+ }
+
+ static function params_getenv(): ?array {
+ $params = getenv("NULIB_APP_app_params");
+ if ($params === false) return null;
+ return unserialize($params);
+ }
+
+ static function get_profile(?bool &$productionMode=null): string {
+ return self::get()->getProfile($productionMode);
+ }
+
+ static function is_prod(): bool {
+ return self::get_profile() === "prod";
+ }
+
+ static function is_devel(): bool {
+ return self::get_profile() === "devel";
+ }
+
+ static function set_profile(?string $profile=null, ?bool $productionMode=null): void {
+ self::get()->setProfile($profile, $productionMode);
+ }
+
+ const FACT_WEB_APP = "web-app";
+ const FACT_CLI_APP = "cli-app";
+
+ static final function is_fact(string $fact, $value=true): bool {
+ return self::get()->isFact($fact, $value);
+ }
+
+ static final function set_fact(string $fact, $value=true): void {
+ self::get()->setFact($fact, $value);
+ }
+
+ static function is_debug(): bool {
+ return self::get()->isDebug();
+ }
+
+ static function set_debug(?bool $debug=true): void {
+ self::get()->setDebug($debug);
+ }
+
+ /**
+ * @var array répertoires vendor exprimés relativement à PROJDIR
+ */
+ const DEFAULT_VENDOR = [
+ "bindir" => "vendor/bin",
+ "autoload" => "vendor/autoload.php",
+ ];
+
+ function __construct(?array $params, bool $useProjParams=false) {
+ if ($useProjParams) {
+ [
+ "projdir" => $projdir,
+ "vendor" => $vendor,
+ "appcode" => $appcode,
+ "datadir" => $datadir,
+ "etcdir" => $etcdir,
+ "vardir" => $vardir,
+ "logdir" => $logdir,
+ ] = $params;
+ $cwd = $params["cwd"] ?? null;
+ $datadirIsDefined = true;
+ } else {
+ # projdir
+ $projdir = $params["projdir"] ?? null;
+ if ($projdir === null) {
+ global $_composer_autoload_path, $_composer_bin_dir;
+ $autoload = $_composer_autoload_path ?? null;
+ $bindir = $_composer_bin_dir ?? null;
+ if ($autoload !== null) {
+ $vendor = preg_replace('/\/[^\/]+\.php$/', "", $autoload);
+ $bindir ??= "$vendor/bin";
+ $projdir = preg_replace('/\/[^\/]+$/', "", $vendor);
+ $params["vendor"] = [
+ "autoload" => $autoload,
+ "bindir" => $bindir,
+ ];
+ }
+ }
+ if ($projdir === null) $projdir = ".";
+ $projdir = path::abspath($projdir);
+ # vendor
+ $vendor = $params["vendor"] ?? self::DEFAULT_VENDOR;
+ $vendor["bindir"] = path::reljoin($projdir, $vendor["bindir"]);
+ $vendor["autoload"] = path::reljoin($projdir, $vendor["autoload"]);
+ # appcode
+ $appcode = $params["appcode"] ?? null;
+ if ($appcode === null) {
+ $appcode = str::without_suffix("-app", path::basename($projdir));
+ }
+ $APPCODE = str_replace("-", "_", strtoupper($appcode));
+ # cwd
+ $cwd = $params["cwd"] ?? null;
+ # datadir
+ $datadir = getenv("${APPCODE}_DATADIR");
+ $datadirIsDefined = $datadir !== false;
+ if ($datadir === false) $datadir = $params["datadir"] ?? null;
+ if ($datadir === null) $datadir = "devel";
+ $datadir = path::reljoin($projdir, $datadir);
+ # etcdir
+ $etcdir = getenv("${APPCODE}_ETCDIR");
+ if ($etcdir === false) $etcdir = $params["etcdir"] ?? null;
+ if ($etcdir === null) $etcdir = "etc";
+ $etcdir = path::reljoin($datadir, $etcdir);
+ # vardir
+ $vardir = getenv("${APPCODE}_VARDIR");
+ if ($vardir === false) $vardir = $params["vardir"] ?? null;
+ if ($vardir === null) $vardir = "var";
+ $vardir = path::reljoin($datadir, $vardir);
+ # logdir
+ $logdir = getenv("${APPCODE}_LOGDIR");
+ if ($logdir === false) $logdir = $params["logdir"] ?? null;
+ if ($logdir === null) $logdir = "log";
+ $logdir = path::reljoin($datadir, $logdir);
+ }
+ # cwd
+ $cwd ??= getcwd();
+ # profile
+ $this->profileManager = new ProfileManager([
+ "app" => true,
+ "name" => $appcode,
+ "default_profile" => $datadirIsDefined? "prod": "devel",
+ "profile" => $params["profile"] ?? null,
+ ]);
+ # $facts
+ $this->facts = $params["facts"] ?? null;
+ # debug
+ $this->debug = $params["debug"] ?? null;
+
+ $this->projdir = $projdir;
+ $this->vendor = $vendor;
+ $this->appcode = $appcode;
+ $this->cwd = $cwd;
+ $this->datadir = $datadir;
+ $this->etcdir = $etcdir;
+ $this->vardir = $vardir;
+ $this->logdir = $logdir;
+
+ # name, title
+ $appgroup = $params["appgroup"] ?? null;
+ $name = $params["name"] ?? $params["class"] ?? null;
+ if ($name === null) {
+ $name = $appcode;
+ } else {
+ # si $name est une classe, enlever le package et normaliser i.e
+ # my\package\MyApplication --> my-application
+ $name = preg_replace('/.*\\\\/', "", $name);
+ $name = str::camel2us($name, false, "-");
+ $name = str::without_suffix("-app", $name);
+ }
+ $this->appgroup = $appgroup;
+ $this->name = $name;
+ $this->title = $params["title"] ?? null;
+ }
+
+ #############################################################################
+ # Paramètres partagés par tous les scripts d'un projet (et les scripts lancés
+ # à partir d'une application de ce projet)
+
+ protected string $projdir;
+
+ function getProjdir(): string {
+ return $this->projdir;
+ }
+
+ protected array $vendor;
+
+ function getVendorBindir(): string {
+ return $this->vendor["bindir"];
+ }
+
+ function getVendorAutoload(): string {
+ return $this->vendor["autoload"];
+ }
+
+ protected string $appcode;
+
+ function getAppcode(): string {
+ return $this->appcode;
+ }
+
+ protected string $cwd;
+
+ function getCwd(): string {
+ return $this->cwd;
+ }
+
+ protected string $datadir;
+
+ function getDatadir(): string {
+ return $this->datadir;
+ }
+
+ protected string $etcdir;
+
+ function getEtcdir(): string {
+ return $this->etcdir;
+ }
+
+ protected string $vardir;
+
+ function getVardir(): string {
+ return $this->vardir;
+ }
+
+ protected string $logdir;
+
+ function getLogdir(): string {
+ return $this->logdir;
+ }
+
+ protected ProfileManager $profileManager;
+
+ function getProfile(?bool &$productionMode=null): string {
+ return $this->profileManager->getProfile($productionMode);
+ }
+
+ function isProductionMode(): bool {
+ return $this->profileManager->isProductionMode();
+ }
+
+ function setProfile(?string $profile, ?bool $productionMode=null): void {
+ $this->profileManager->setProfile($profile, $productionMode);
+ }
+
+ protected ?array $facts;
+
+ function isFact(string $fact, $value=true): bool {
+ return ($this->facts[$fact] ?? false) === $value;
+ }
+
+ function setFact(string $fact, $value=true): void {
+ $this->facts[$fact] = $value;
+ }
+
+ protected ?bool $debug;
+
+ function isDebug(): bool {
+ $debug = $this->debug;
+ if ($debug === null) {
+ $debug = defined("DEBUG")? DEBUG: null;
+ $DEBUG = getenv("DEBUG");
+ $debug ??= $DEBUG !== false? $DEBUG: null;
+ $debug ??= config::k("debug");
+ $debug ??= false;
+ $this->debug = $debug;
+ }
+ return $debug;
+ }
+
+ function setDebug(bool $debug=true): void {
+ $this->debug = $debug;
+ }
+
+ /**
+ * @param ?string|false $profile
+ *
+ * false === pas de profil
+ * null === profil par défaut
+ */
+ function withProfile(string $file, $profile): string {
+ if ($profile !== false) {
+ $profile ??= $this->getProfile();
+ [$dir, $filename] = path::split($file);
+ $basename = path::basename($filename);
+ $ext = path::ext($file);
+ $file = path::join($dir, "$basename.$profile$ext");
+ }
+ return $file;
+ }
+
+ function findFile(array $dirs, array $names, $profile=null): string {
+ # d'abord chercher avec le profil
+ if ($profile !== false) {
+ foreach ($dirs as $dir) {
+ foreach ($names as $name) {
+ $file = path::join($dir, $name);
+ $file = $this->withProfile($file, $profile);
+ if (file_exists($file)) return $file;
+ }
+ }
+ }
+ # puis sans profil
+ foreach ($dirs as $dir) {
+ foreach ($names as $name) {
+ $file = path::join($dir, $name);
+ if (file_exists($file)) return $file;
+ }
+ }
+ # la valeur par défaut est avec profil
+ return $this->withProfile(path::join($dirs[0], $names[0]), $profile);
+ }
+
+ function fencedJoin(string $basedir, ?string ...$paths): string {
+ $path = path::reljoin($basedir, ...$paths);
+ if (!path::is_within($path, $basedir)) {
+ throw ValueException::invalid_value($path, "path");
+ }
+ return $path;
+ }
+
+ #############################################################################
+ # Paramètres spécifiques à cette application
+
+ protected ?string $appgroup;
+
+ function getAppgroup(): ?string {
+ return $this->appgroup;
+ }
+
+ protected string $name;
+
+ function getName(): ?string {
+ return $this->name;
+ }
+
+ protected ?string $title;
+
+ function getTitle(): ?string {
+ return $this->title;
+ }
+
+ #############################################################################
+ # Méthodes outils
+
+ /** recréer le tableau des paramètres */
+ function getParams(): array {
+ return [
+ "projdir" => $this->projdir,
+ "vendor" => $this->vendor,
+ "appcode" => $this->appcode,
+ "cwd" => $this->cwd,
+ "datadir" => $this->datadir,
+ "etcdir" => $this->etcdir,
+ "vardir" => $this->vardir,
+ "logdir" => $this->logdir,
+ "profile" => $this->getProfile(),
+ "facts" => $this->facts,
+ "debug" => $this->debug,
+ "appgroup" => $this->appgroup,
+ "name" => $this->name,
+ "title" => $this->title,
+ ];
+ }
+
+ /**
+ * obtenir le chemin vers le fichier de configuration. par défaut, retourner
+ * une valeur de la forme "$ETCDIR/$name[.$profile].conf"
+ */
+ function getEtcfile(?string $name=null, $profile=null): string {
+ if ($name === null) $name = "{$this->name}.conf";
+ return $this->findFile([$this->etcdir], [$name], $profile);
+ }
+
+ /**
+ * obtenir le chemin vers le fichier de travail. par défaut, retourner une
+ * valeur de la forme "$VARDIR/$appgroup/$name[.$profile].tmp"
+ */
+ function getVarfile(?string $name=null, $profile=null): string {
+ if ($name === null) $name = "{$this->name}.tmp";
+ $file = $this->fencedJoin($this->vardir, $this->appgroup, $name);
+ $file = $this->withProfile($file, $profile);
+ sh::mkdirof($file);
+ return $file;
+ }
+
+ /**
+ * obtenir le chemin vers le fichier de log. par défaut, retourner une
+ * valeur de la forme "$LOGDIR/$appgroup/$name.log" (sans le profil, parce
+ * qu'il s'agit du fichier de log par défaut)
+ *
+ * Si $name est spécifié, la valeur retournée sera de la forme
+ * "$LOGDIR/$appgroup/$basename[.$profile].$ext"
+ */
+ function getLogfile(?string $name=null, $profile=null): string {
+ if ($name === null) {
+ $name = "{$this->name}.log";
+ $profile ??= false;
+ }
+ $file = $this->fencedJoin($this->logdir, $this->appgroup, $name);
+ $file = $this->withProfile($file, $profile);
+ sh::mkdirof($file);
+ return $file;
+ }
+
+ /**
+ * obtenir le chemin absolu vers un fichier de travail
+ * - si le chemin est absolu, il est inchangé
+ * - sinon le chemin est exprimé par rapport à $vardir/$appgroup
+ *
+ * is $ensureDir, créer le répertoire du fichier s'il n'existe pas déjà
+ *
+ * la différence avec {@link self::getVarfile()} est que le fichier peut
+ * au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de
+ * valeur par défaut pour $file
+ */
+ function getWorkfile(string $file, $profile=null, bool $ensureDir=true): string {
+ $file = path::reljoin($this->vardir, $this->appgroup, $file);
+ $file = $this->withProfile($file, $profile);
+ if ($ensureDir) sh::mkdirof($file);
+ return $file;
+ }
+
+ /**
+ * obtenir le chemin absolu vers un fichier spécifié par l'utilisateur.
+ * - si le chemin commence par /, il est laissé en l'état
+ * - si le chemin commence par ./ ou ../, il est exprimé par rapport à $cwd
+ * - sinon le chemin est exprimé par rapport à $vardir/$appgroup
+ *
+ * la différence est avec {@link self::getVarfile()} est que le fichier peut
+ * au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de
+ * valeur par défaut pour $file
+ */
+ function getUserfile(string $file): string {
+ if (path::is_qualified($file)) {
+ return path::reljoin($this->cwd, $file);
+ } else {
+ return path::reljoin($this->vardir, $this->appgroup, $file);
+ }
+ }
+
+ protected ?RunFile $runfile = null;
+
+ function getRunfile(): RunFile {
+ $name = $this->name;
+ $runfile = $this->getWorkfile($name);
+ $logfile = $this->getLogfile("$name.out", false);
+ return $this->runfile ??= new RunFile($name, $runfile, $logfile);
+ }
+
+ protected ?array $lockFiles = null;
+
+ function getLockfile(?string $name=null): LockFile {
+ $this->lockFiles[$name] ??= $this->getRunfile()->getLockFile($name, $this->title);
+ return $this->lockFiles[$name];
+ }
+
+ #############################################################################
+
+ const EC_FORK_CHILD = 250;
+ const EC_FORK_PARENT = 251;
+ const EC_DISABLED = 252;
+ const EC_LOCKED = 253;
+ const EC_BAD_COMMAND = 254;
+ const EC_UNEXPECTED = 255;
+
+ #############################################################################
+
+ static bool $dispach_signals = false;
+
+ static function install_signal_handler(bool $allow=true): void {
+ if (!$allow) return;
+ $signalHandler = function(int $signo, $siginfo) {
+ throw new ExitError(128 + $signo);
+ };
+ pcntl_signal(SIGHUP, $signalHandler);
+ pcntl_signal(SIGINT, $signalHandler);
+ pcntl_signal(SIGQUIT, $signalHandler);
+ pcntl_signal(SIGTERM, $signalHandler);
+ self::$dispach_signals = true;
+ }
+
+ static function _dispatch_signals() {
+ if (self::$dispach_signals) pcntl_signal_dispatch();
+ }
+
+ #############################################################################
+
+ static ?func $bgapplication_enabled = null;
+
+ /**
+ * spécifier la fonction permettant de vérifier si l'exécution de tâches
+ * de fond est autorisée. Si cette méthode n'est pas utilisée, par défaut,
+ * les tâches planifiées sont autorisées
+ *
+ * si $func===true, spécifier une fonction qui retourne toujours vrai
+ * si $func===false, spécifiée une fonction qui retourne toujours faux
+ * sinon, $func doit être une fonction valide
+ */
+ static function set_bgapplication_enabled($func): void {
+ if (is_bool($func)) {
+ $enabled = $func;
+ $func = function () use ($enabled) {
+ return $enabled;
+ };
+ }
+ self::$bgapplication_enabled = func::with($func);
+ }
+
+ /**
+ * Si les exécutions en tâche de fond sont autorisée, retourner. Sinon
+ * afficher une erreur et quitter l'application
+ */
+ static function check_bgapplication_enabled(bool $forceEnabled=false): void {
+ if (self::$bgapplication_enabled === null || $forceEnabled) return;
+ if (!self::$bgapplication_enabled->invoke()) {
+ throw new ExitError(self::EC_DISABLED, "Planifications désactivées. La tâche n'a pas été lancée");
+ }
+ }
+
+ #############################################################################
+
+ static function action(?string $title, ?int $maxSteps=null): void {
+ self::get()->getRunfile()->action($title, $maxSteps);
+ }
+
+ static function step(int $nbSteps=1): void {
+ self::get()->getRunfile()->step($nbSteps);
+ }
+}
diff --git a/php/src/app/args/AbstractArgsParser.php b/php/src/app/args/AbstractArgsParser.php
new file mode 100644
index 0000000..1a50037
--- /dev/null
+++ b/php/src/app/args/AbstractArgsParser.php
@@ -0,0 +1,109 @@
+ 0) throw $this->notEnoughArgs($count, $option);
+ }
+
+ protected function tooManyArgs(int $count, int $expected, ?string $arg=null): ArgsException {
+ if ($arg !== null) $arg .= ": ";
+ return new ArgsException("${arg}trop d'arguments (attendu $expected, reçu $count)");
+ }
+
+ protected function invalidArg(string $arg): ArgsException {
+ return new ArgsException("$arg: argument invalide");
+ }
+
+ protected function ambiguousArg(string $arg, array $candidates): ArgsException {
+ $candidates = implode(", ", $candidates);
+ return new ArgsException("$arg: argument ambigû (les valeurs possibles sont $candidates)");
+ }
+
+ /**
+ * consommer les arguments de $src en avançant l'index $srci et provisionner
+ * $dest à partir de $desti. si $desti est plus grand que 0, celà veut dire
+ * que $dest a déjà commencé à être provisionné, et qu'il faut continuer.
+ *
+ * $destmin est le nombre minimum d'arguments à consommer. $destmax est le
+ * nombre maximum d'arguments à consommer.
+ *
+ * $srci est la position de l'élément courant à consommer le cas échéant
+ * retourner le nombre d'arguments qui manquent (ou 0 si tous les arguments
+ * ont été consommés)
+ *
+ * pour les arguments optionnels, ils sont consommés tant qu'il y en a de
+ * disponible, ou jusqu'à la présence de '--'. Si $keepsep, l'argument '--'
+ * est gardé dans la liste des arguments optionnels.
+ */
+ protected static function consume_args($src, &$srci, &$dest, $desti, $destmin, $destmax, bool $keepsep): int {
+ $srcmax = count($src);
+ # arguments obligatoires
+ while ($desti < $destmin) {
+ if ($srci < $srcmax) {
+ $dest[] = $src[$srci];
+ } else {
+ # pas assez d'arguments
+ return $destmin - $desti;
+ }
+ $srci++;
+ $desti++;
+ }
+ # arguments facultatifs
+ $eoo = false; // l'option a-t-elle été terminée?
+ while ($desti < $destmax && $srci < $srcmax) {
+ $opt = $src[$srci];
+ $srci++;
+ $desti++;
+ if ($opt === "--") {
+ # fin des arguments facultatifs en entrée
+ $eoo = true;
+ if ($keepsep) $dest[] = $opt;
+ break;
+ }
+ $dest[] = $opt;
+ }
+ if (!$eoo && $desti < $destmax) {
+ # pas assez d'arguments en entrée, terminer avec "--"
+ $dest[] = "--";
+ }
+ return 0;
+ }
+
+ abstract function normalize(array $args): array;
+
+ /** @var object|array objet destination */
+ protected $dest;
+
+ protected function setDest(&$dest): void {
+ $this->dest =& $dest;
+ }
+
+ protected function unsetDest(): void {
+ unset($this->dest);
+ }
+
+ abstract function process(array $args);
+
+ function parse(&$dest, array $args=null): void {
+ if ($args === null) {
+ global $argv;
+ $args = array_slice($argv, 1);
+ }
+ $args = $this->normalize($args);
+ $dest ??= new stdClass();
+ $this->setDest($dest);
+ $this->process($args);
+ $this->unsetDest();
+ }
+
+ abstract function actionPrintHelp(string $arg): void;
+}
diff --git a/php/src/app/args/Aodef.php b/php/src/app/args/Aodef.php
new file mode 100644
index 0000000..58b3a73
--- /dev/null
+++ b/php/src/app/args/Aodef.php
@@ -0,0 +1,623 @@
+origDef = $def;
+ $this->mergeParse($def);
+ //$this->debugTrace("construct");
+ }
+
+ protected array $origDef;
+
+ public bool $show = true;
+ public ?bool $disabled = null;
+ public ?bool $isRemains = null;
+ public ?string $extends = null;
+
+ protected ?array $_removes = null;
+ protected ?array $_adds = null;
+
+ protected ?array $_args = null;
+ public ?string $argsdesc = null;
+
+ public ?bool $ensureArray = null;
+ public $action = null;
+ public ?func $func = null;
+ public ?bool $inverse = null;
+ public $value = null;
+ public ?string $name = null;
+ public ?string $property = null;
+ public ?string $key = null;
+
+ public ?string $help = null;
+
+ protected ?array $_options = [];
+
+ public bool $haveShortOptions = false;
+ public bool $haveLongOptions = false;
+ public bool $isCommand = false;
+ public bool $isHelp = false;
+
+ public bool $haveArgs = false;
+ public ?int $minArgs = null;
+ public ?int $maxArgs = null;
+
+ protected function mergeParse(array $def): void {
+ $merges = $defs["merges"] ?? null;
+ $merge = $defs["merge"] ?? null;
+ if ($merge !== null) $merges[] = $merge;
+ if ($merges !== null) {
+ foreach ($merges as $merge) {
+ if ($merge !== null) $this->mergeParse($merge);
+ }
+ }
+
+ $this->parse($def);
+
+ $merge = $defs["merge_after"] ?? null;
+ if ($merge !== null) $this->mergeParse($merge);
+ }
+
+ protected function parse(array $def): void {
+ [$options, $params] = cl::split_assoc($def);
+
+ $this->show ??= $params["show"] ?? true;
+ $this->extends ??= $params["extends"] ?? null;
+
+ $this->disabled = vbool::withn($params["disabled"] ?? null);
+ $removes = varray::withn($params["remove"] ?? null);
+ A::merge($this->_removes, $removes);
+ $adds = varray::withn($params["add"] ?? null);
+ A::merge($this->_adds, $adds);
+ A::merge($this->_adds, $options);
+
+ $args = $params["args"] ?? null;
+ $args ??= $params["arg"] ?? null;
+ if ($args === true) $args = 1;
+ elseif ($args === "*") $args = [null];
+ elseif ($args === "+") $args = ["value", null];
+ if (is_int($args)) $args = array_fill(0, $args, "value");
+ $this->_args ??= cl::withn($args);
+
+ $this->argsdesc ??= $params["argsdesc"] ?? null;
+
+ $this->ensureArray ??= $params["ensure_array"] ?? null;
+ $this->action = $params["action"] ?? null;
+ $this->inverse ??= $params["inverse"] ?? null;
+ $this->value ??= $params["value"] ?? null;
+ $this->name ??= $params["name"] ?? null;
+ $this->property ??= $params["property"] ?? null;
+ $this->key ??= $params["key"] ?? null;
+
+ $this->help ??= $params["help"] ?? null;
+ }
+
+ function isExtends(): bool {
+ return $this->extends !== null;
+ }
+
+ function setup1(bool $extends=false, ?Aolist $aolist=null): void {
+ if (!$extends && !$this->isExtends()) {
+ $this->processOptions();
+ } elseif ($extends && $this->isExtends()) {
+ $this->processExtends($aolist);
+ }
+ $this->initRemains();
+ //$this->debugTrace("setup1");
+ }
+
+ protected function processExtends(Aolist $argdefs): void {
+ $option = $this->extends;
+ if ($option === null) {
+ throw ArgsException::missing("extends", "destination arg");
+ }
+ $dest = $argdefs->get($option);
+ if ($dest === null) {
+ throw ArgsException::invalid($option, "destination arg");
+ }
+
+ if ($this->ensureArray !== null) $dest->ensureArray = $this->ensureArray;
+ if ($this->action !== null) $dest->action = $this->action;
+ if ($this->inverse !== null) $dest->inverse = $this->inverse;
+ if ($this->value !== null) $dest->value = $this->value;
+ if ($this->name !== null) $dest->name = $this->name;
+ if ($this->property !== null) $dest->property = $this->property;
+ if ($this->key !== null) $dest->key = $this->key;
+
+ A::merge($dest->_removes, $this->_removes);
+ A::merge($dest->_adds, $this->_adds);
+ $dest->processOptions();
+ }
+
+ function buildOptions(?array $options): array {
+ $result = [];
+ if ($options !== null) {
+ foreach ($options as $option) {
+ if (substr($option, 0, 2) === "--") {
+ $type = self::TYPE_LONG;
+ if (preg_match('/^--([^:-][^:]*)(::?)?$/', $option, $ms)) {
+ $name = $ms[1];
+ $args = $ms[2] ?? null;
+ $option = "--$name";
+ } else {
+ throw ArgsException::invalid($option, "long option");
+ }
+ } elseif (substr($option, 0, 1) === "-") {
+ $type = self::TYPE_SHORT;
+ if (preg_match('/^-([^:-])(::?)?$/', $option, $ms)) {
+ $name = $ms[1];
+ $args = $ms[2] ?? null;
+ $option = "-$name";
+ } else {
+ throw ArgsException::invalid($option, "short option");
+ }
+ } else {
+ $type = self::TYPE_COMMAND;
+ if (preg_match('/^([^:-][^:]*)$/', $option, $ms)) {
+ $name = $ms[1];
+ $args = null;
+ $option = "$name";
+ } else {
+ throw ArgsException::invalid($option, "command");
+ }
+ }
+ if ($args === ":") {
+ $argsType = self::ARGS_MANDATORY;
+ } elseif ($args === "::") {
+ $argsType = self::ARGS_OPTIONAL;
+ } else {
+ $argsType = self::ARGS_NONE;
+ }
+ $result[$option] = [
+ "name" => $name,
+ "option" => $option,
+ "type" => $type,
+ "args_type" => $argsType,
+ ];
+ }
+ }
+ return $result;
+ }
+
+ protected function initRemains(): void {
+ if ($this->isRemains === null) {
+ $options = array_fill_keys(array_keys($this->_options), true);
+ foreach (array_keys($this->buildOptions($this->_removes)) as $option) {
+ unset($options[$option]);
+ }
+ foreach (array_keys($this->buildOptions($this->_adds)) as $option) {
+ unset($options[$option]);
+ }
+ if (!$options) $this->isRemains = true;
+ }
+ }
+
+ /** traiter le paramètre parent */
+ protected function processOptions(): void {
+ $this->removeOptions($this->_removes);
+ $this->_removes = null;
+ $this->addOptions($this->_adds);
+ $this->_adds = null;
+ }
+
+ function addOptions(?array $options): void {
+ A::merge($this->_options, $this->buildOptions($options));
+ $this->updateType();
+ }
+
+ function removeOptions(?array $options): void {
+ foreach ($this->buildOptions($options) as $option) {
+ unset($this->_options[$option["option"]]);
+ }
+ $this->updateType();
+ }
+
+ function removeOption(string $option): void {
+ unset($this->_options[$option]);
+ }
+
+ /** mettre à jour le type d'option */
+ protected function updateType(): void {
+ $haveShortOptions = false;
+ $haveLongOptions = false;
+ $isCommand = false;
+ $isHelp = false;
+ foreach ($this->_options as $option) {
+ switch ($option["type"]) {
+ case self::TYPE_SHORT:
+ $haveShortOptions = true;
+ break;
+ case self::TYPE_LONG:
+ $haveLongOptions = true;
+ break;
+ case self::TYPE_COMMAND:
+ $isCommand = true;
+ break;
+ }
+ switch ($option["option"]) {
+ case "--help":
+ case "--help++":
+ $isHelp = true;
+ break;
+ }
+ }
+ $this->haveShortOptions = $haveShortOptions;
+ $this->haveLongOptions = $haveLongOptions;
+ $this->isCommand = $isCommand;
+ $this->isHelp = $isHelp;
+ }
+
+ function setup2(): void {
+ $this->processArgs();
+ $this->processAction();
+ $this->afterSetup();
+ //$this->debugTrace("setup2");
+ }
+
+ /**
+ * traiter les informations concernant les arguments puis calculer les nombres
+ * minimum et maximum d'arguments que prend l'option
+ */
+ protected function processArgs(): void {
+ $args = $this->_args;
+ $haveArgs = boolval($args);
+ if ($this->isRemains) {
+ $haveArgs = true;
+ $args = [null];
+ } elseif ($args === null) {
+ $optionalArgs = null;
+ foreach ($this->_options as $option) {
+ switch ($option["args_type"]) {
+ case self::ARGS_NONE:
+ break;
+ case self::ARGS_MANDATORY:
+ $haveArgs = true;
+ $optionalArgs = false;
+ break;
+ case self::ARGS_OPTIONAL:
+ $haveArgs = true;
+ $optionalArgs ??= true;
+ break;
+ }
+ }
+ $optionalArgs ??= false;
+ if ($haveArgs) {
+ $args = ["value"];
+ if ($optionalArgs) $args = [$args];
+ }
+ }
+
+ if ($this->isRemains) $desc = "remaining args";
+ else $desc = cl::first($this->_options)["option"];
+
+ $args ??= [];
+ $argsdesc = [];
+ $reqs = [];
+ $haveNull = false;
+ $optArgs = null;
+ foreach ($args as $arg) {
+ if (is_string($arg)) {
+ $reqs[] = $arg;
+ $argsdesc[] = strtoupper($arg);
+ } elseif (is_array($arg)) {
+ $optArgs = $arg;
+ break;
+ } elseif ($arg === null) {
+ $haveNull = true;
+ break;
+ } else {
+ throw ArgsException::invalid("$desc: $arg", "option arg");
+ }
+ }
+
+ $opts = [];
+ $optArgsdesc = null;
+ $lastarg = "VALUE";
+ if ($optArgs !== null) {
+ $haveOpt = false;
+ foreach ($optArgs as $arg) {
+ if (is_string($arg)) {
+ $haveOpt = true;
+ $opts[] = $arg;
+ $lastarg = strtoupper($arg);
+ $optArgsdesc[] = $lastarg;
+ } elseif ($arg === null) {
+ $haveNull = true;
+ break;
+ } else {
+ throw ArgsException::invalid("$desc: $arg", "option arg");
+ }
+ }
+ if (!$haveOpt) $haveNull = true;
+ }
+ if ($haveNull) $optArgsdesc[] = "${lastarg}s...";
+ if ($optArgsdesc !== null) {
+ $argsdesc[] = "[".implode(" ", $optArgsdesc)."]";
+ }
+
+ $minArgs = count($reqs);
+ if ($haveNull) $maxArgs = PHP_INT_MAX;
+ else $maxArgs = $minArgs + count($opts);
+
+ $this->haveArgs = $haveArgs;
+ $this->minArgs = $minArgs;
+ $this->maxArgs = $maxArgs;
+ $this->argsdesc ??= implode(" ", $argsdesc);
+ }
+
+ private static function get_longest(array $options, int $type): ?string {
+ $longest = null;
+ $maxlen = 0;
+ foreach ($options as $option) {
+ if ($option["type"] !== $type) continue;
+ $name = $option["name"];
+ $len = strlen($name);
+ if ($len > $maxlen) {
+ $longest = $name;
+ $maxlen = $len;
+ }
+ }
+ return $longest;
+ }
+
+ protected function processAction(): void {
+ $this->ensureArray ??= $this->isRemains || $this->maxArgs > 1;
+
+ $action = $this->action;
+ $func = $this->func;
+ if ($action === null) {
+ if ($this->isCommand) $action = "--set-command";
+ elseif ($this->isRemains) $action = "--set-args";
+ elseif ($this->isHelp) $action = "--show-help";
+ elseif ($this->haveArgs) $action = "--set";
+ elseif ($this->value !== null) $action = "--set";
+ else $action = "--inc";
+ }
+ if (is_string($action) && substr($action, 0, 2) === "--") {
+ # fonction interne
+ } else {
+ $func = func::with($action);
+ $action = "--func";
+ }
+ $this->action = $action;
+ $this->func = $func;
+
+ $name = $this->name;
+ $property = $this->property;
+ $key = $this->key;
+ if ($action !== "--func" && !$this->isRemains &&
+ $name === null && $property === null && $key === null
+ ) {
+ # si on ne précise pas le nom de la propriété, la dériver à partir du
+ # nom de l'option la plus longue
+ $longest = self::get_longest($this->_options, self::TYPE_LONG);
+ $longest ??= self::get_longest($this->_options, self::TYPE_COMMAND);
+ $longest ??= self::get_longest($this->_options, self::TYPE_SHORT);
+ if ($longest !== null) {
+ $longest = preg_replace('/[^A-Za-z0-9]+/', "_", $longest);
+ if (preg_match('/^[0-9]/', $longest)) {
+ # le nom de la propriété ne doit pas commencer par un chiffre
+ $longest = "p$longest";
+ }
+ $name = $longest;
+ }
+ } elseif ($name === null && $property !== null) {
+ $name = $property;
+ } elseif ($name === null && $key !== null) {
+ $name = $key;
+ }
+ $this->name = $name;
+ }
+
+ protected function afterSetup(): void {
+ $this->disabled ??= false;
+ $this->ensureArray ??= false;
+ $this->inverse ??= false;
+ if (str::del_prefix($this->help, "++")) {
+ $this->show = false;
+ }
+ }
+
+ function getOptions(): array {
+ if ($this->disabled) return [];
+ else return array_keys($this->_options);
+ }
+
+ function isEmpty(): bool {
+ return $this->disabled || !$this->_options;
+ }
+
+ function printHelp(?array $what=null): void {
+ $showDef = $what["show"] ?? $this->show;
+ if (!$showDef) return;
+
+ $prefix = $what["prefix"] ?? null;
+ if ($prefix !== null) echo $prefix;
+
+ $showOptions = $what["options"] ?? true;
+ if ($showOptions) {
+ echo " ";
+ echo implode(", ", array_keys($this->_options));
+ if ($this->haveArgs) {
+ echo " ";
+ echo $this->argsdesc;
+ }
+ echo "\n";
+ }
+
+ $showHelp = $what["help"] ?? true;
+ if ($this->help && $showHelp) {
+ echo str::indent($this->help, " ");
+ echo "\n";
+ }
+ }
+
+ function action(&$dest, $value, ?string $arg, AbstractArgsParser $parser): void {
+ if ($this->ensureArray) {
+ varray::ensure($value);
+ } elseif (is_array($value)) {
+ $count = count($value);
+ if ($count == 0) $value = null;
+ elseif ($count == 1) $value = $value[0];
+ }
+
+ switch ($this->action) {
+ case "--set": $this->actionSet($dest, $value); break;
+ case "--inc": $this->actionInc($dest); break;
+ case "--dec": $this->actionDec($dest); break;
+ case "--add": $this->actionAdd($dest, $value); break;
+ case "--adds": $this->actionAdds($dest, $value); break;
+ case "--merge": $this->actionMerge($dest, $value); break;
+ case "--merges": $this->actionMerges($dest, $value); break;
+ case "--func": $this->func->bind($dest)->invoke([$value, $arg, $this]); break;
+ case "--set-args": $this->actionSetArgs($dest, $value); break;
+ case "--set-command": $this->actionSetCommand($dest, $value); break;
+ case "--show-help": $parser->actionPrintHelp($arg); break;
+ default: throw ArgsException::invalid($this->action, "arg action");
+ }
+ }
+
+ function actionSet(&$dest, $value): void {
+ if ($this->property !== null) {
+ oprop::set($dest, $this->property, $value);
+ } elseif ($this->key !== null) {
+ akey::set($dest, $this->key, $value);
+ } elseif ($this->name !== null) {
+ valx::set($dest, $this->name, $value);
+ }
+ }
+
+ function actionInc(&$dest): void {
+ if ($this->property !== null) {
+ if ($this->inverse) oprop::dec($dest, $this->property);
+ else oprop::inc($dest, $this->property);
+ } elseif ($this->key !== null) {
+ if ($this->inverse) akey::dec($dest, $this->key);
+ else akey::inc($dest, $this->key);
+ } elseif ($this->name !== null) {
+ if ($this->inverse) valx::dec($dest, $this->name);
+ else valx::inc($dest, $this->name);
+ }
+ }
+
+ function actionDec(&$dest): void {
+ if ($this->property !== null) {
+ if ($this->inverse) oprop::inc($dest, $this->property);
+ else oprop::dec($dest, $this->property);
+ } elseif ($this->key !== null) {
+ if ($this->inverse) akey::inc($dest, $this->key);
+ else akey::dec($dest, $this->key);
+ } elseif ($this->name !== null) {
+ if ($this->inverse) valx::inc($dest, $this->name);
+ else valx::dec($dest, $this->name);
+ }
+ }
+
+ function actionAdd(&$dest, $value): void {
+ if ($this->property !== null) {
+ oprop::append($dest, $this->property, $value);
+ } elseif ($this->key !== null) {
+ akey::append($dest, $this->key, $value);
+ } elseif ($this->name !== null) {
+ valx::append($dest, $this->name, $value);
+ }
+ }
+
+ function actionAdds(&$dest, $value): void {
+ if ($this->property !== null) {
+ foreach (cl::with($value) as $value) {
+ oprop::append($dest, $this->property, $value);
+ }
+ } elseif ($this->key !== null) {
+ foreach (cl::with($value) as $value) {
+ akey::append($dest, $this->key, $value);
+ }
+ } elseif ($this->name !== null) {
+ foreach (cl::with($value) as $value) {
+ valx::append($dest, $this->name, $value);
+ }
+ }
+ }
+
+ function actionMerge(&$dest, $value): void {
+ if ($this->property !== null) {
+ oprop::merge($dest, $this->property, $value);
+ } elseif ($this->key !== null) {
+ akey::merge($dest, $this->key, $value);
+ } elseif ($this->name !== null) {
+ valx::merge($dest, $this->name, $value);
+ }
+ }
+
+ function actionMerges(&$dest, $value): void {
+ if ($this->property !== null) {
+ foreach (cl::with($value) as $value) {
+ oprop::merge($dest, $this->property, $value);
+ }
+ } elseif ($this->key !== null) {
+ foreach (cl::with($value) as $value) {
+ akey::merge($dest, $this->key, $value);
+ }
+ } elseif ($this->name !== null) {
+ foreach (cl::with($value) as $value) {
+ valx::merge($dest, $this->name, $value);
+ }
+ }
+ }
+
+ function actionSetArgs(&$dest, $value): void {
+ if ($this->property !== null) {
+ oprop::set($dest, $this->property, $value);
+ } elseif ($this->key !== null) {
+ akey::set($dest, $this->key, $value);
+ } elseif ($this->name !== null) {
+ valx::set($dest, $this->name, $value);
+ }
+ }
+
+ function actionSetCommand(&$dest, $value): void {
+ if ($this->property !== null) {
+ oprop::set($dest, $this->property, $value);
+ } elseif ($this->key !== null) {
+ akey::set($dest, $this->key, $value);
+ } elseif ($this->name !== null) {
+ valx::set($dest, $this->name, $value);
+ }
+ }
+
+ function __toString(): string {
+ $options = implode(",", $this->getOptions());
+ $args = $this->haveArgs? " ({$this->minArgs}-{$this->maxArgs})": false;
+ return "$options$args";
+ }
+ private function debugTrace(string $message): void {
+ $options = implode(",", cl::split_assoc($this->origDef)[0] ?? []);
+ echo "$options $message\n";
+ }
+}
diff --git a/php/src/app/args/Aogroup.php b/php/src/app/args/Aogroup.php
new file mode 100644
index 0000000..7873a70
--- /dev/null
+++ b/php/src/app/args/Aogroup.php
@@ -0,0 +1,38 @@
+all() as $aodef) {
+ $firstAodef ??= $aodef;
+ $aodef->printHelp(["help" => false]);
+ }
+ if ($firstAodef !== null) {
+ $firstAodef->printHelp(["options" => false]);
+ }
+ }
+}
diff --git a/php/src/app/args/Aolist.php b/php/src/app/args/Aolist.php
new file mode 100644
index 0000000..504de50
--- /dev/null
+++ b/php/src/app/args/Aolist.php
@@ -0,0 +1,271 @@
+origDefs = $defs;
+ $this->initDefs($defs, $setup);
+ }
+
+ protected array $origDefs;
+
+ protected ?array $aomain;
+ protected ?array $aosections;
+ protected ?array $aospecials;
+
+ public ?Aodef $remainsArgdef = null;
+
+ function initDefs(array $defs, bool $setup=true): void {
+ $this->mergeParse($defs, $aobjects);
+ $this->aomain = $aobjects["main"] ?? null;
+ $this->aosections = $aobjects["sections"] ?? null;
+ $this->aospecials = $aobjects["specials"] ?? null;
+ if ($setup) $this->setup();
+ }
+
+ protected function mergeParse(array $defs, ?array &$aobjects, bool $parse=true): void {
+ $aobjects ??= [];
+
+ $merges = $defs["merges"] ?? null;
+ $merge = $defs["merge"] ?? null;
+ if ($merge !== null) $merges[] = $merge;
+ if ($merges !== null) {
+ foreach ($merges as $merge) {
+ $this->mergeParse($merge, $aobjects, false);
+ $this->parse($merge, $aobjects);
+ }
+ }
+
+ if ($parse) $this->parse($defs, $aobjects);
+
+ $merge = $defs["merge_after"] ?? null;
+ if ($merge !== null) {
+ $this->mergeParse($merge, $aobjects, false);
+ $this->parse($merge, $aobjects);
+ }
+ }
+
+ protected function parse(array $defs, array &$aobjects): void {
+ [$defs, $params] = cl::split_assoc($defs);
+ if ($defs !== null) {
+ $aomain =& $aobjects["main"];
+ foreach ($defs as $def) {
+ $first = $def[0] ?? null;
+ if ($first === "group") {
+ $aobject = new Aogroup($def);
+ } else {
+ $aobject = new Aodef($def);
+ }
+ $aomain[] = $aobject;
+ }
+ }
+ $sections = $params["sections"] ?? null;
+ if ($sections !== null) {
+ $aosections =& $aobjects["sections"];
+ $index = 0;
+ foreach ($sections as $key => $section) {
+ if ($key === $index) {
+ $index++;
+ $aosections[] = new Aosection($section);
+ } else {
+ /** @var Aosection $aosection */
+ $aosection = $aosections[$key] ?? null;
+ if ($aosection === null) {
+ $aosections[$key] = new Aosection($section);
+ } else {
+ #XXX il faut implémenter la fusion en cas de section existante
+ # pour le moment, la liste existante est écrasée
+ $aosection->initDefs($section);
+ }
+ }
+ }
+ }
+ $this->parseParams($params);
+ }
+
+ protected function parseParams(?array $params): void {
+ }
+
+ function all(?array $what=null): iterable {
+ $returnsAodef = $what["aodef"] ?? true;
+ $returnsAolist = $what["aolist"] ?? false;
+ $returnExtends = $what["extends"] ?? false;
+ $withSpecials = $what["aospecials"] ?? true;
+ # lister les sections avant, pour que les options de la section principale
+ # soient prioritaires
+ $aosections = $this->aosections;
+ if ($aosections !== null) {
+ /** @var Aosection $aobject */
+ foreach ($aosections as $aosection) {
+ if ($returnsAolist) {
+ yield $aosection;
+ } elseif ($returnsAodef) {
+ yield from $aosection->all($what);
+ }
+ }
+ }
+
+ $aomain = $this->aomain;
+ if ($aomain !== null) {
+ /** @var Aodef $aobject */
+ foreach ($aomain as $aobject) {
+ if ($aobject instanceof Aodef) {
+ if ($returnsAodef) {
+ if ($returnExtends) {
+ if ($aobject->isExtends()) yield $aobject;
+ } else {
+ if (!$aobject->isExtends()) yield $aobject;
+ }
+ }
+ } elseif ($aobject instanceof Aolist) {
+ if ($returnsAolist) {
+ yield $aobject;
+ } elseif ($returnsAodef) {
+ yield from $aobject->all($what);
+ }
+ }
+ }
+ }
+
+ $aospecials = $this->aospecials;
+ if ($withSpecials && $aospecials !== null) {
+ /** @var Aodef $aobject */
+ foreach ($aospecials as $aobject) {
+ yield $aobject;
+ }
+ }
+ }
+
+ protected function filter(callable $callback): void {
+ $aomain = $this->aomain;
+ if ($aomain !== null) {
+ $filtered = [];
+ /** @var Aodef $aobject */
+ foreach ($aomain as $aobject) {
+ if ($aobject instanceof Aolist) {
+ $aobject->filter($callback);
+ }
+ if (call_user_func($callback, $aobject)) {
+ $filtered[] = $aobject;
+ }
+ }
+ $this->aomain = $filtered;
+ }
+ $aosections = $this->aosections;
+ if ($aosections !== null) {
+ $filtered = [];
+ /** @var Aosection $aosection */
+ foreach ($aosections as $aosection) {
+ $aosection->filter($callback);
+ if (call_user_func($callback, $aosection)) {
+ $filtered[] = $aosection;
+ }
+ }
+ $this->aosections = $filtered;
+ }
+ }
+
+ protected function setup(): void {
+ # calculer les options
+ foreach ($this->all() as $aodef) {
+ $aodef->setup1();
+ }
+ /** @var Aodef $aodef */
+ foreach ($this->all(["extends" => true]) as $aodef) {
+ $aodef->setup1(true, $this);
+ }
+ # ne garder que les objets non vides
+ $this->filter(function($aobject): bool {
+ if ($aobject instanceof Aodef) {
+ return !$aobject->isEmpty();
+ } elseif ($aobject instanceof Aolist) {
+ return !$aobject->isEmpty();
+ } else {
+ return false;
+ }
+ });
+ # puis calculer nombre d'arguments et actions
+ foreach ($this->all() as $aodef) {
+ $aodef->setup2();
+ }
+ }
+
+ function isEmpty(): bool {
+ foreach ($this->all() as $aobject) {
+ return false;
+ }
+ return true;
+ }
+
+ function get(string $option): ?Aodef {
+ return null;
+ }
+
+ function actionPrintHelp(string $arg): void {
+ $this->printHelp([
+ "show_all" => $arg === "--help++",
+ ]);
+ }
+
+ function printHelp(?array $what=null): void {
+ $show = $what["show_all"] ?? false;
+ if (!$show) $show = null;
+
+ $aosections = $this->aosections;
+ if ($aosections !== null) {
+ /** @var Aosection $aosection */
+ foreach ($aosections as $aosection) {
+ $aosection->printHelp(cl::merge($what, [
+ "show" => $show,
+ "prefix" => "\n",
+ ]));
+ }
+ }
+
+ $aomain = $this->aomain;
+ if ($aomain !== null) {
+ echo "\nOPTIONS\n";
+ foreach ($aomain as $aobject) {
+ $aobject->printHelp(cl::merge($what, [
+ "show" => $show,
+ ]));
+ }
+ }
+ }
+
+ function __toString(): string {
+ $items = [];
+ $what = [
+ "aodef" => true,
+ "aolist" => true,
+ ];
+ foreach ($this->all($what) as $aobject) {
+ if ($aobject instanceof Aodef) {
+ $items[] = strval($aobject);
+ } elseif ($aobject instanceof Aogroup) {
+ $items[] = implode("\n", [
+ "group",
+ str::indent(strval($aobject)),
+ ]);
+ } elseif ($aobject instanceof Aosection) {
+ $items[] = implode("\n", [
+ "section",
+ str::indent(strval($aobject)),
+ ]);
+ } else {
+ $items[] = false;
+ }
+ }
+ return implode("\n", $items);
+ }
+}
diff --git a/php/src/app/args/Aosection.php b/php/src/app/args/Aosection.php
new file mode 100644
index 0000000..b5d478c
--- /dev/null
+++ b/php/src/app/args/Aosection.php
@@ -0,0 +1,47 @@
+show = vbool::with($params["show"] ?? true);
+ $this->prefix ??= $params["prefix"] ?? null;
+ $this->title ??= $params["title"] ?? null;
+ $this->description ??= $params["description"] ?? null;
+ $this->suffix ??= $params["suffix"] ?? null;
+ }
+
+ function printHelp(?array $what=null): void {
+ $showSection = $what["show"] ?? $this->show;
+ if (!$showSection) return;
+
+ $prefix = $what["prefix"] ?? null;
+ if ($prefix !== null) echo $prefix;
+
+ if ($this->prefix) echo "{$this->prefix}\n";
+ if ($this->title) echo "{$this->title}\n";
+ if ($this->description) echo "\n{$this->description}\n";
+ /** @var Aodef|Aolist $aobject */
+ foreach ($this->all(["aolist" => true]) as $aobject) {
+ $aobject->printHelp();
+ }
+ if ($this->suffix) echo "{$this->suffix}\n";
+ }
+}
diff --git a/php/src/app/args/ArgsException.php b/php/src/app/args/ArgsException.php
new file mode 100644
index 0000000..db2cfff
--- /dev/null
+++ b/php/src/app/args/ArgsException.php
@@ -0,0 +1,20 @@
+prefix ??= $params["prefix"] ?? null;
+ $this->name ??= $params["name"] ?? null;
+ $this->purpose ??= $params["purpose"] ?? null;
+ $this->usage ??= $params["usage"] ?? null;
+ $this->description ??= $params["description"] ?? null;
+ $this->suffix ??= $params["suffix"] ?? null;
+
+ $this->commandname ??= $params["commandname"] ?? null;
+ $this->commandproperty ??= $params["commandproperty"] ?? null;
+ $this->commandkey ??= $params["commandkey"] ?? null;
+
+ $this->argsname ??= $params["argsname"] ?? null;
+ $this->argsproperty ??= $params["argsproperty"] ?? null;
+ $this->argskey ??= $params["argskey"] ?? null;
+
+ $this->autohelp ??= vbool::withn($params["autohelp"] ?? null);
+ $this->autoremains ??= vbool::withn($params["autoremains"] ?? null);
+ }
+
+ /** @return string[] */
+ function getOptions(): array {
+ return array_keys($this->index);
+ }
+
+ protected function indexAodefs(): void {
+ $this->index = [];
+ foreach ($this->all() as $aodef) {
+ $options = $aodef->getOptions();
+ foreach ($options as $option) {
+ /** @var Aodef $prevAodef */
+ $prevAodef = $this->index[$option] ?? null;
+ if ($prevAodef !== null) $prevAodef->removeOption($option);
+ $this->index[$option] = $aodef;
+ }
+ }
+ }
+
+ protected function setup(): void {
+ # calculer les options pour les objets déjà fusionnés
+ /** @var Aodef $aodef */
+ foreach ($this->all() as $aodef) {
+ $aodef->setup1();
+ }
+
+ # puis traiter les extensions d'objets et calculer les options pour ces
+ # objets sur la base de l'index que l'on crée une première fois
+ $this->indexAodefs();
+ /** @var Aodef $aodef */
+ foreach ($this->all(["extends" => true]) as $aodef) {
+ $aodef->setup1(true, $this);
+ }
+
+ # ne garder que les objets non vides
+ $this->filter(function($aobject) {
+ if ($aobject instanceof Aodef) {
+ return !$aobject->isEmpty();
+ } elseif ($aobject instanceof Aolist) {
+ return !$aobject->isEmpty();
+ } else {
+ return false;
+ }
+ });
+
+ # rajouter remains et help si nécessaire
+ $this->aospecials = [];
+ $helpArgdef = null;
+ $remainsArgdef = null;
+ /** @var Aodef $aodef */
+ foreach ($this->all() as $aodef) {
+ if ($aodef->isHelp) $helpArgdef = $aodef;
+ if ($aodef->isRemains) $remainsArgdef = $aodef;
+ }
+
+ $this->autohelp ??= true;
+ if ($helpArgdef === null && $this->autohelp) {
+ $helpArgdef = new Aodef([
+ "--help", "--help++",
+ "action" => "--show-help",
+ "help" => "Afficher l'aide",
+ ]);
+ $helpArgdef->setup1();
+ }
+ if ($helpArgdef !== null) $this->aospecials[] = $helpArgdef;
+
+ $this->autoremains ??= true;
+ if ($remainsArgdef === null && $this->autoremains) {
+ $remainsArgdef = new Aodef([
+ "args" => [null],
+ "action" => "--set-args",
+ "name" => $this->argsname ?? "args",
+ "property" => $this->argsproperty,
+ "key" => $this->argskey,
+ ]);
+ $remainsArgdef->setup1();
+ }
+ if ($remainsArgdef !== null) {
+ $this->remainsArgdef = $remainsArgdef;
+ $this->aospecials[] = $remainsArgdef;
+ }
+
+ # puis calculer nombre d'arguments et actions
+ $this->indexAodefs();
+ /** @var Aodef $aodef */
+ foreach ($this->all() as $aodef) {
+ $aodef->setup2();
+ }
+ }
+
+ function get(string $option): ?Aodef {
+ return $this->index[$option] ?? null;
+ }
+
+ function printHelp(?array $what = null): void {
+ $showList = $what["show"] ?? true;
+ if (!$showList) return;
+
+ $prefix = $what["prefix"] ?? null;
+ if ($prefix !== null) echo $prefix;
+
+ if ($this->prefix) echo "{$this->prefix}\n";
+ if ($this->purpose) {
+ echo "{$this->name}: {$this->purpose}\n";
+ } elseif (!$this->prefix) {
+ # s'il y a un préfixe sans purpose, il remplace purpose
+ echo "{$this->name}\n";
+ }
+ if ($this->usage) {
+ echo "\nUSAGE\n";
+ foreach (cl::with($this->usage) as $usage) {
+ echo " {$this->name} $usage\n";
+ }
+ }
+ if ($this->description) echo "\n{$this->description}\n";
+ parent::printHelp($what);
+ if ($this->suffix) echo "{$this->suffix}\n";
+ }
+
+ function __toString(): string {
+ return implode("\n", [
+ "objects:",
+ str::indent(parent::__toString()),
+ "index:",
+ str::indent(implode("\n", array_keys($this->index))),
+ ]);
+ }
+}
diff --git a/php/src/app/args/SimpleArgsParser.php b/php/src/app/args/SimpleArgsParser.php
new file mode 100644
index 0000000..e514dc1
--- /dev/null
+++ b/php/src/app/args/SimpleArgsParser.php
@@ -0,0 +1,250 @@
+aolist = new SimpleAolist($defs);
+ }
+
+ protected SimpleAolist $aolist;
+
+ protected function getArgdef(string $option): ?Aodef {
+ return $this->aolist->get($option);
+ }
+
+ protected function getOptions(): array {
+ return $this->aolist->getOptions();
+ }
+
+ function normalize(array $args): array {
+ $i = 0;
+ $max = count($args);
+ $options = [];
+ $remains = [];
+ $parseOpts = true;
+ while ($i < $max) {
+ $arg = $args[$i++];
+ if (!$parseOpts) {
+ # le reste n'est que des arguments
+ $remains[] = $arg;
+ continue;
+ }
+ if ($arg === "--") {
+ # fin des options
+ $parseOpts = false;
+ continue;
+ }
+
+ if (substr($arg, 0, 2) === "--") {
+ #######################################################################
+ # option longue
+ $pos = strpos($arg, "=");
+ if ($pos !== false) {
+ # option avec valeur
+ $option = substr($arg, 0, $pos);
+ $value = substr($arg, $pos + 1);
+ } else {
+ # option sans valeur
+ $option = $arg;
+ $value = null;
+ }
+ $argdef = $this->getArgdef($option);
+ if ($argdef === null) {
+ # chercher une correspondance
+ $len = strlen($option);
+ $candidates = [];
+ foreach ($this->getOptions() as $candidate) {
+ if (substr($candidate, 0, $len) === $option) {
+ $candidates[] = $candidate;
+ }
+ }
+ switch (count($candidates)) {
+ case 0: throw $this->invalidArg($option);
+ case 1: $option = $candidates[0]; break;
+ default: throw $this->ambiguousArg($option, $candidates);
+ }
+ $argdef = $this->getArgdef($option);
+ }
+
+ if ($argdef->haveArgs) {
+ $minArgs = $argdef->minArgs;
+ $maxArgs = $argdef->maxArgs;
+ $values = [];
+ if ($value !== null) {
+ $values[] = $value;
+ $offset = 1;
+ } elseif ($minArgs == 0) {
+ # cas particulier: la première valeur doit être collée à l'option
+ # si $maxArgs == 1
+ $offset = $maxArgs == 1 ? 1 : 0;
+ } else {
+ $offset = 0;
+ }
+ $this->checkEnoughArgs($option,
+ self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true));
+
+ if ($minArgs == 0 && $maxArgs == 1) {
+ # cas particulier: la première valeur doit être collée à l'option
+ if (count($values) > 0) {
+ $options[] = "$option=$values[0]";
+ $values = array_slice($values, 1);
+ } else {
+ $options[] = $option;
+ }
+ } else {
+ $options[] = $option;
+ }
+ $options = array_merge($options, $values);
+ } elseif ($value !== null) {
+ throw $this->tooManyArgs(1, 0, $option);
+ } else {
+ $options[] = $option;
+ }
+
+ } elseif (substr($arg, 0, 1) === "-") {
+ #######################################################################
+ # option courte
+ $pos = 1;
+ $len = strlen($arg);
+ while ($pos < $len) {
+ $option = "-".substr($arg, $pos, 1);
+ $argdef = $this->getArgdef($option);
+ if ($argdef === null) throw $this->invalidArg($option);
+ if ($argdef->haveArgs) {
+ $minArgs = $argdef->minArgs;
+ $maxArgs = $argdef->maxArgs;
+ $values = [];
+ if ($len > $pos + 1) {
+ $values[] = substr($arg, $pos + 1);
+ $offset = 1;
+ $pos = $len;
+ } elseif ($minArgs == 0) {
+ # cas particulier: la première valeur doit être collée à l'option
+ # si $maxArgs == 1
+ $offset = $maxArgs == 1 ? 1 : 0;
+ } else {
+ $offset = 0;
+ }
+ $this->checkEnoughArgs($option,
+ self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true));
+
+ if ($minArgs == 0 && $maxArgs == 1) {
+ # cas particulier: la première valeur doit être collée à l'option
+ if (count($values) > 0) {
+ $options[] = "$option$values[0]";
+ $values = array_slice($values, 1);
+ } else {
+ $options[] = $option;
+ }
+ } else {
+ $options[] = $option;
+ }
+ $options = array_merge($options, $values);
+ } else {
+ $options[] = $option;
+ }
+ $pos++;
+ }
+ } else {
+ #XXX implémenter les commandes
+
+ #######################################################################
+ # argument
+ $remains[] = $arg;
+ }
+ }
+ return array_merge($options, ["--"], $remains);
+ }
+
+ function process(array $args) {
+ $i = 0;
+ $max = count($args);
+ # d'abord traiter les options
+ while ($i < $max) {
+ $arg = $args[$i++];
+ if ($arg === "--") {
+ # fin des options
+ break;
+ }
+
+ if (preg_match('/^(--[^=]+)(?:=(.*))?/', $arg, $ms)) {
+ # option longue
+ } elseif (preg_match('/^(-.)(.+)?/', $arg, $ms)) {
+ # option courte
+ } else {
+ # commande
+ throw StateException::unexpected_state("commands are not supported");
+ }
+ $option = $ms[1];
+ $ovalue = $ms[2] ?? null;
+ $argdef = $this->getArgdef($option);
+ if ($argdef === null) throw StateException::unexpected_state();
+ $defvalue = $argdef->value;
+ if ($argdef->haveArgs) {
+ $minArgs = $argdef->minArgs;
+ $maxArgs = $argdef->maxArgs;
+ if ($minArgs == 0 && $maxArgs == 1) {
+ # argument facultatif
+ if ($ovalue !== null) $value = [$ovalue];
+ else $value = cl::with($defvalue);
+ $offset = 1;
+ } else {
+ $value = [];
+ $offset = 0;
+ }
+ self::consume_args($args, $i, $value, $offset, $minArgs, $maxArgs, false);
+ } else {
+ $value = $defvalue;
+ }
+
+ $this->action($value, $arg, $argdef);
+ }
+
+ # construire la liste des arguments qui restent
+ $args = array_slice($args, $i);
+ $i = 0;
+ $max = count($args);
+ $argdef = $this->aolist->remainsArgdef;
+ if ($argdef !== null && $argdef->haveArgs) {
+ $minArgs = $argdef->minArgs;
+ $maxArgs = $argdef->maxArgs;
+ if ($maxArgs == PHP_INT_MAX) {
+ # cas particulier: si le nombre d'arguments restants est non borné,
+ # les prendre tous sans distinction ni traitement de '--'
+ $value = $args;
+ # mais tester tout de même s'il y a le minimum requis d'arguments
+ $this->checkEnoughArgs(null, $minArgs - $max);
+ } else {
+ $value = [];
+ $this->checkEnoughArgs(null,
+ self::consume_args($args, $i, $value, 0, $minArgs, $maxArgs, false));
+ if ($i <= $max - 1) throw $this->tooManyArgs($max, $i);
+ }
+ $this->action($value, null, $argdef);
+ } elseif ($i <= $max - 1) {
+ throw $this->tooManyArgs($max, $i);
+ }
+ }
+
+ function action($value, ?string $arg, Aodef $argdef) {
+ $argdef->action($this->dest, $value, $arg, $this);
+ }
+
+ public function actionPrintHelp(string $arg): void {
+ $this->aolist->actionPrintHelp($arg);
+ throw new ExitError(0);
+ }
+
+ function showDebugInfos() {
+ echo $this->aolist."\n"; #XXX
+ }
+}
diff --git a/php/src/app/args/TODO.md b/php/src/app/args/TODO.md
new file mode 100644
index 0000000..d19db42
--- /dev/null
+++ b/php/src/app/args/TODO.md
@@ -0,0 +1,21 @@
+# nulib\app\args
+
+* [ ] dans la section "profils", rajouter une option pour spécifier un fichier de configuration
+* [ ] transformer un schéma en définition d'arguments, un tableau en liste d'arguments, et vice-versa
+* [ ] faire une implémentation ArgsParser qui supporte les commandes, et les options dynamiques
+ * commandes:
+ `program [options] command [options]`
+ * multi-commandes:
+ `program [options] command [options] // command [options] // ...`
+ * dynamique: la liste des options et des commandes supportées est calculée dynamiquement
+
+## support des commandes
+
+faire une interface Runnable qui représente un composant pouvant être exécuté.
+Application implémente Runnable, mais l'analyse des arguments peut retourner une
+autre instance de runnable pour faciliter l'implémentation de différents
+sous-outils
+
+## BUGS
+
+-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary
\ No newline at end of file
diff --git a/php/src/app/cli/Application.php b/php/src/app/cli/Application.php
new file mode 100644
index 0000000..5a947aa
--- /dev/null
+++ b/php/src/app/cli/Application.php
@@ -0,0 +1,383 @@
+getDesc();
+ echo implode("\n", $desc["message"])."\n";
+ $ec = $desc["exitcode"] ?? 0;
+ break;
+ case "dump":
+ case "d":
+ yaml::dump($runfile->read());
+ break;
+ case "reset":
+ case "z":
+ if (!$runfile->isRunning()) $runfile->reset();
+ else $ec = self::_error("cannot reset while running");
+ break;
+ case "release":
+ case "rl":
+ $runfile->release();
+ break;
+ case "start":
+ case "s":
+ array_splice($argv, 1, 1); $argc--;
+ return;
+ case "kill":
+ case "k":
+ if ($runfile->isRunning()) $runfile->wfKill();
+ else $ec = self::_error("not running");
+ break;
+ default:
+ $ec = self::_error("$argv[1]: unexpected command", app::EC_BAD_COMMAND);
+ }
+ exit($ec);
+ }
+
+ static function run(?Application $app=null): void {
+ $unlock = false;
+ $stop = false;
+ $shutdown = function () use (&$unlock, &$stop) {
+ if ($unlock) {
+ app::get()->getRunfile()->release();
+ $unlock = false;
+ }
+ if ($stop) {
+ app::get()->getRunfile()->wfStop();
+ $stop = false;
+ }
+ };
+ register_shutdown_function($shutdown);
+ app::install_signal_handler(static::INSTALL_SIGNAL_HANDLER);
+ try {
+ static::_initialize_app();
+ $useRunfile = static::USE_RUNFILE;
+ $useRunlock = static::USE_RUNLOCK;
+ if ($useRunfile) {
+ $runfile = app::get()->getRunfile();
+
+ global $argc, $argv;
+ self::_manage_runfile($argc, $argv, $runfile);
+ if ($useRunlock && $runfile->warnIfLocked()) exit(app::EC_LOCKED);
+
+ $runfile->wfStart();
+ $stop = true;
+ if ($useRunlock) {
+ $runfile->lock();
+ $unlock = true;
+ }
+ }
+ if ($app === null) $app = new static();
+ static::_configure_app($app);
+ static::_start_app($app);
+ } catch (ExitError $e) {
+ if ($e->haveUserMessage()) msg::error($e->getUserMessage());
+ exit($e->getCode());
+ } catch (Exception $e) {
+ msg::error($e);
+ exit(app::EC_UNEXPECTED);
+ }
+ }
+
+ protected static function _initialize_app(): void {
+ app::init(static::class);
+ app::set_fact(app::FACT_CLI_APP);
+ msg::set_messenger(new StdMessenger([
+ "min_level" => msg::DEBUG,
+ ]));
+ }
+
+ protected static function _configure_app(Application $app): void {
+ config::configure(config::CONFIGURE_INITIAL_ONLY);
+
+ $msgs = null;
+ $msgs["console"] = new StdMessenger([
+ "min_level" => msg::NORMAL,
+ ]);
+ if (static::USE_LOGFILE) {
+ $msgs["log"] = new StdMessenger([
+ "output" => app::get()->getLogfile(),
+ "min_level" => msg::MINOR,
+ "add_date" => true,
+ ]);
+ }
+ msg::init($msgs);
+
+ $app->parseArgs();
+ config::configure();
+ }
+
+ protected static function _start_app(Application $app): void {
+ $retcode = $app->main();
+ if (is_int($retcode)) exit($retcode);
+ elseif (is_bool($retcode)) exit($retcode? 0: 1);
+ elseif ($retcode !== null) exit(strval($retcode));
+ }
+
+ /**
+ * sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e
+ * pas d'erreur)
+ *
+ * équivalent à lancer l'exception {@link ExitError}
+ */
+ protected static final function exit(int $exitcode=0, $message=null) {
+ throw new ExitError($exitcode, $message);
+ }
+
+ /**
+ * sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e
+ * une erreur s'est produite)
+ *
+ * équivalent à lancer l'exception {@link ExitError}
+ */
+ protected static final function die($message=null, int $exitcode=1) {
+ throw new ExitError($exitcode, $message);
+ }
+
+ const PROFILE_SECTION = [
+ "title" => "PROFILS D'EXECUTION",
+ "show" => false,
+ ["group",
+ ["-p", "--profile", "--app-profile",
+ "args" => "profile",
+ "action" => [app::class, "set_profile"],
+ "help" => "spécifier le profil d'exécution",
+ ],
+ ["-P", "--prod", "action" => [app::class, "set_profile", "prod"]],
+ ["-T", "--test", "action" => [app::class, "set_profile", "test"]],
+ ["--devel", "action" => [app::class, "set_profile", "devel"]],
+ ],
+ ];
+
+ const VERBOSITY_SECTION = [
+ "title" => "NIVEAU D'INFORMATION",
+ "show" => false,
+ ["group",
+ ["--verbosity",
+ "args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug",
+ "action" => [null, "set_application_verbosity"],
+ "help" => "spécifier le niveau d'informations affiché",
+ ],
+ ["-q", "--quiet", "action" => [null, "set_application_verbosity", "quiet"]],
+ ["-v", "--verbose", "action" => [null, "set_application_verbosity", "verbose"]],
+ ["-D", "--debug", "action" => [null, "set_application_verbosity", "debug"]],
+ ],
+ ["-L", "--logfile",
+ "args" => "output",
+ "action" => [null, "set_application_log_output"],
+ "help" => "Logger les messages de l'application dans le fichier spécifié",
+ ],
+ ["group",
+ ["--color",
+ "action" => [null, "set_application_color", true],
+ "help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut",
+ ],
+ ["--no-color", "action" => [null, "set_application_color", false]],
+ ],
+ ];
+
+ static function set_application_verbosity(string $verbosity): void {
+ $console = console::get();
+ switch ($verbosity) {
+ case "Q":
+ case "silent":
+ $console->resetParams([
+ "min_level" => msg::NONE,
+ ]);
+ break;
+ case "q":
+ case "quiet":
+ $console->resetParams([
+ "min_level" => msg::MAJOR,
+ ]);
+ break;
+ case "n":
+ case "normal":
+ $console->resetParams([
+ "min_level" => msg::NORMAL,
+ ]);
+ break;
+ case "v":
+ case "verbose":
+ $console->resetParams([
+ "min_level" => msg::MINOR,
+ ]);
+ break;
+ case "D":
+ case "debug":
+ app::set_debug();
+ $console->resetParams([
+ "min_level" => msg::DEBUG,
+ ]);
+ break;
+ default:
+ throw ValueException::invalid_value($verbosity, "verbosity");
+ }
+ }
+
+ static function set_application_log_output(string $logfile): void {
+ log::create_or_reset_params([
+ "output" => $logfile,
+ ], StdMessenger::class, [
+ "add_date" => true,
+ "min_level" => log::MINOR,
+ ]);
+ }
+ static function set_application_color(bool $color): void {
+ console::reset_params([
+ "color" => $color,
+ ]);
+ }
+ const ARGS = [
+ "sections" => [
+ self::PROFILE_SECTION,
+ self::VERBOSITY_SECTION,
+ ],
+ ];
+
+ protected function getArgsParser(): AbstractArgsParser {
+ return new SimpleArgsParser(static::ARGS);
+ }
+
+ /** @throws ArgsException */
+ function parseArgs(array $args=null): void {
+ $this->getArgsParser()->parse($this, $args);
+ }
+
+ const PROFILE_COLORS = [
+ "prod" => "@r",
+ "test" => "@g",
+ "devel" => "@w",
+ ];
+ const DEFAULT_PROFILE_COLOR = "y";
+
+ /** retourner le profil courant en couleur */
+ static function get_profile(?string $profile=null): string {
+ if ($profile === null) $profile = app::get_profile();
+ foreach (static::PROFILE_COLORS as $text => $color) {
+ if (strpos($profile, $text) !== false) {
+ return $color? "$profile": $profile;
+ }
+ }
+ $color = static::DEFAULT_PROFILE_COLOR;
+ return $color? "$profile": $profile;
+ }
+
+ protected ?array $args = null;
+
+ abstract function main();
+
+ static function runfile(): RunFile {
+ return app::with(static::class)->getRunfile();
+ }
+}
diff --git a/php/src/app/config.php b/php/src/app/config.php
new file mode 100644
index 0000000..08699f9
--- /dev/null
+++ b/php/src/app/config.php
@@ -0,0 +1,41 @@
+addConfigurator($configurators);
+ }
+
+ # certains types de configurations sont normalisés
+ /** ne configurer que le minimum pour que l'application puisse s'initialiser */
+ const CONFIGURE_INITIAL_ONLY = ["include" => "initial"];
+ /** ne configurer que les routes */
+ const CONFIGURE_ROUTES_ONLY = ["include" => "routes"];
+ /** configurer uniquement ce qui ne nécessite pas d'avoir une session */
+ const CONFIGURE_NO_SESSION = ["exclude" => "session"];
+
+ static function configure(?array $params=null): void {
+ self::$config->configure($params);
+ }
+
+ static final function add($config, string ...$profiles): void { self::$config->addConfig($config, $profiles); }
+ static final function get(string $pkey, $default=null, ?string $profile=null) { return self::$config->getValue($pkey, $default, $profile); }
+ static final function k(string $pkey, $default=null) { return self::$config->getValue("app.$pkey", $default); }
+ static final function db(string $pkey, $default=null) { return self::$config->getValue("dbs.$pkey", $default); }
+ static final function m(string $pkey, $default=null) { return self::$config->getValue("msgs.$pkey", $default); }
+ static final function l(string $pkey, $default=null) { return self::$config->getValue("mails.$pkey", $default); }
+}
+
+new class extends config {
+ function __construct() {
+ self::$config = new ConfigManager();
+ }
+};
\ No newline at end of file
diff --git a/php/src/app/config/ArrayConfig.php b/php/src/app/config/ArrayConfig.php
new file mode 100644
index 0000000..6a02c8e
--- /dev/null
+++ b/php/src/app/config/ArrayConfig.php
@@ -0,0 +1,50 @@
+APP(); break;
+ case "dbs": $default = $this->DBS(); break;
+ case "msgs": $default = $this->MSGS(); break;
+ case "mails": $default = $this->MAILS(); break;
+ default: $default = [];
+ }
+ $config[$key] ??= $default;
+ }
+ $this->config = $config;
+ }
+
+ protected array $config;
+
+ function has(string $pkey, string $profile): bool {
+ return cl::phas($this->config, $pkey);
+ }
+
+ function get(string $pkey, string $profile) {
+ return cl::pget($this->config, $pkey);
+ }
+
+ function set(string $pkey, $value, string $profile): void {
+ cl::pset($this->config, $pkey, $value);
+ }
+}
diff --git a/php/src/app/config/ConfigManager.php b/php/src/app/config/ConfigManager.php
new file mode 100644
index 0000000..ef10881
--- /dev/null
+++ b/php/src/app/config/ConfigManager.php
@@ -0,0 +1,150 @@
+configurators, cl::with($configurators));
+ }
+
+ protected array $configured = [];
+
+ /**
+ * configurer les objets et les classes qui ne l'ont pas encore été. la liste
+ * des objets et des classes à configurer est fournie en appelant la méthode
+ * {@link addConfigurator()}
+ *
+ * par défaut, la configuration se fait en appelant toutes les méthodes
+ * publiques des objets et toutes les méthodes statiques des classes qui
+ * commencent par 'configure', e.g 'configureThis()' ou 'configure_db()',
+ * si elles n'ont pas déjà été appelées
+ *
+ * Il est possible de modifier la liste des méthodes appelées avec le tableau
+ * $params, qui doit être conforme au schema de {@link func::CALL_ALL_SCHEMA}
+ */
+ function configure(?array $params=null): void {
+ $params["prefix"] ??= "configure";
+ foreach ($this->configurators as $key => $configurator) {
+ $configured =& $this->configured[$key];
+ /** @var func[] $methods */
+ $methods = func::get_all($configurator, $params);
+ foreach ($methods as $method) {
+ $name = $method->getName() ?? "(no name)";
+ $done = $configured[$name] ?? false;
+ if (!$done) {
+ $method->invoke();
+ $configured[$name] = true;
+ }
+ }
+ }
+ }
+
+ #############################################################################
+
+ protected $cache = [];
+
+ protected function resetCache(): void {
+ $this->cache = [];
+ }
+
+ protected function cacheHas(string $pkey, string $profile) {
+ return array_key_exists("$profile.$pkey", $this->cache);
+ }
+
+ protected function cacheGet(string $pkey, string $profile) {
+ return cl::get($this->cache, "$profile.$pkey");
+ }
+
+ protected function cacheSet(string $pkey, $value, string $profile): void {
+ $this->cache["$profile.$pkey"] = $value;
+ }
+
+ protected array $profileConfigs = [];
+
+ /**
+ * Ajouter une configuration valide pour le(s) profil(s) spécifié(s)
+ *
+ * $config est un objet ou une classe qui définit une ou plusieurs des
+ * constantes APP, DBS, MSGS, MAILS
+ *
+ * si $inProfiles===null, la configuration est valide dans tous les profils
+ */
+ function addConfig($config, ?array $inProfiles=null): void {
+ if (is_string($config)) {
+ $c = new ReflectionClass($config);
+ if ($c->implementsInterface(IConfig::class)) {
+ $config = $c->newInstance();
+ } else {
+ $config = [];
+ foreach (IConfig::CONFIG_KEYS as $key) {
+ $config[$key] = cl::with($c->getConstant(strtoupper($key)));
+ }
+ $config = new ArrayConfig($config);
+ }
+ } elseif (is_array($config)) {
+ $config = new ArrayConfig($config);
+ } elseif (!($config instanceof IConfig)) {
+ throw ValueException::invalid_type($config, "array|IConfig");
+ }
+
+ $inProfiles ??= [IConfig::PROFILE_ALL];
+ foreach ($inProfiles as $profile) {
+ $this->profileConfigs[$profile][] = $config;
+ }
+
+ $this->resetCache();
+ }
+
+ function _getValue(string $pkey, $default, string $inProfile) {
+ $profiles = [$inProfile];
+ if ($inProfile !== IConfig::PROFILE_ALL) $profiles[] = IConfig::PROFILE_ALL;
+ $value = $default;
+ foreach ($profiles as $profile) {
+ /** @var IConfig[] $configs */
+ $configs = $this->profileConfigs[$profile] ?? [];
+ foreach (array_reverse($configs) as $config) {
+ if ($config->has($pkey, $profile)) {
+ $value = $config->get($pkey, $profile);
+ break;
+ }
+ }
+ }
+ return $value;
+ }
+
+ /**
+ * obtenir la valeur au chemin de clé $pkey dans le profil spécifié
+ *
+ * le $inProfile===null, prendre le profil par défaut.
+ */
+ function getValue(string $pkey, $default=null, ?string $inProfile=null) {
+ $inProfile ??= app::get_profile();
+
+ if ($this->cacheHas($pkey, $inProfile)) {
+ return $this->cacheGet($pkey, $inProfile);
+ }
+
+ $value = $this->_getValue($pkey, $default, $inProfile);
+ $this->cacheSet($pkey, $default, $inProfile);
+ return $value;
+ }
+
+ function setValue(string $pkey, $value, ?string $inProfile=null): void {
+ $inProfile ??= app::get_profile();
+ /** @var IConfig[] $configs */
+ $configs =& $this->profileConfigs[$inProfile];
+ if ($configs === null) $key = 0;
+ else $key = array_key_last($configs);
+ $configs[$key] ??= new ArrayConfig([]);
+ $configs[$key]->set($pkey, $value, $inProfile);
+ }
+}
diff --git a/php/src/app/config/EnvConfig.php b/php/src/app/config/EnvConfig.php
new file mode 100644
index 0000000..b07bf47
--- /dev/null
+++ b/php/src/app/config/EnvConfig.php
@@ -0,0 +1,112 @@
+ "mysql", "name" => "mysql:host=authdb;dbname=auth;charset=utf8",
+ * "user" => "auth_int", "pass" => "auth" ]
+ * situé au chemin de clé dbs.auth dans le profil prod, on peut par exemple
+ * définir les variables suivantes:
+ * CONFIG_prod_dbs__auth__type="mysql"
+ * CONFIG_prod_dbs__auth__name="mysql:host=authdb;dbname=auth;charset=utf8"
+ * CONFIG_prod_dbs__auth__user="auth_int"
+ * CONFIG_prod_dbs__auth__pass="auth"
+ * ou alternativement:
+ * JSON_CONFIG_prod_dbs__auth='{"type":"mysql","name":"mysql:host=authdb;dbname=auth;charset=utf8","user":"auth_int","pass":"auth"}'
+ *
+ * Les préfixes supportés sont, dans l'ordre de précédence:
+ * - JSON_FILE_CONFIG -- une valeur au format JSON inscrite dans un fichier
+ * - JSON_CONFIG -- une valeur au format JSON
+ * - FILE_CONFIG -- une valeur inscrite dans un fichier
+ * - CONFIG -- une valeur scalaire
+ */
+class EnvConfig implements IConfig{
+ protected ?array $profileConfigs = null;
+
+ /** analyser $name et retourner [$pkey, $profile] */
+ private static function parse_pkey_profile($name): array {
+ $i = strpos($name, "_");
+ if ($i === false) return [false, false];
+ $profile = substr($name, 0, $i);
+ if ($profile === "ALL") $profile = IConfig::PROFILE_ALL;
+ $name = substr($name, $i + 1);
+ $pkey = str_replace("__", ".", $name);
+ return [$pkey, $profile];
+ }
+
+ function loadEnvConfig(): void {
+ if ($this->profileConfigs !== null) return;
+ $json_files = [];
+ $jsons = [];
+ $files = [];
+ $vars = [];
+ foreach (getenv() as $name => $value) {
+ if (str::starts_with("JSON_FILE_CONFIG_", $name)) {
+ $json_files[str::without_prefix("JSON_FILE_CONFIG_", $name)] = $value;
+ } elseif (str::starts_with("JSON_CONFIG_", $name)) {
+ $jsons[str::without_prefix("JSON_CONFIG_", $name)] = $value;
+ } elseif (str::starts_with("FILE_CONFIG_", $name)) {
+ $files[str::without_prefix("FILE_CONFIG_", $name)] = $value;
+ } elseif (str::starts_with("CONFIG_", $name)) {
+ $vars[str::without_prefix("CONFIG_", $name)] = $value;
+ }
+ }
+ $profileConfigs = [];
+ foreach ($json_files as $name => $file) {
+ [$pkey, $profile] = self::parse_pkey_profile($name);
+ $value = json::load($file);
+ cl::pset($profileConfigs, "$profile.$pkey", $value);
+ }
+ foreach ($jsons as $name => $json) {
+ [$pkey, $profile] = self::parse_pkey_profile($name);
+ $value = json::decode($json);
+ cl::pset($profileConfigs, "$profile.$pkey", $value);
+ }
+ foreach ($files as $name => $file) {
+ [$pkey, $profile] = self::parse_pkey_profile($name);
+ $value = file::reader($file)->getContents();
+ cl::pset($profileConfigs, "$profile.$pkey", $value);
+ }
+ foreach ($vars as $name => $value) {
+ [$pkey, $profile] = self::parse_pkey_profile($name);
+ cl::pset($profileConfigs, "$profile.$pkey", $value);
+ }
+ $this->profileConfigs = $profileConfigs;
+ }
+
+ function has(string $pkey, string $profile): bool {
+ $this->loadEnvConfig();
+ $config = $this->profileConfigs[$profile] ?? null;
+ return cl::phas($config, $pkey);
+ }
+
+ function get(string $pkey, string $profile) {
+ $this->loadEnvConfig();
+ $config = $this->profileConfigs[$profile] ?? null;
+ return cl::pget($config, $pkey);
+ }
+
+ function set(string $pkey, $value, string $profile): void {
+ $this->loadEnvConfig();
+ $config =& $this->profileConfigs[$profile];
+ cl::pset($config, $pkey, $value);
+ }
+}
diff --git a/php/src/app/config/IConfig.php b/php/src/app/config/IConfig.php
new file mode 100644
index 0000000..dcba89f
--- /dev/null
+++ b/php/src/app/config/IConfig.php
@@ -0,0 +1,24 @@
+ true,
+ "test" => true,
+ ];
+
+ /**
+ * @var array mapping profil d'application --> profil effectif
+ *
+ * ce mapping est utilisé quand il faut calculer le profil courant s'il n'a
+ * pas été spécifié par l'utilisateur. il permet de faire correspondre le
+ * profil courant de l'application avec le profil effectif à sélectionner
+ */
+ const PROFILE_MAP = null;
+
+ function __construct(?array $params=null) {
+ $this->isAppProfile = $params["app"] ?? false;
+ $this->profiles = static::PROFILES;
+ $this->productionModes = static::PRODUCTION_MODES;
+ $this->profileMap = static::PROFILE_MAP;
+ $name = $params["name"] ?? static::NAME;
+ if ($name === null) {
+ $this->configKey = null;
+ $this->envKeys = ["APP_PROFILE"];
+ } else {
+ $configKey = "${name}_profile";
+ $envKey = strtoupper($configKey);
+ if ($this->isAppProfile) {
+ $this->configKey = null;
+ $this->envKeys = [$envKey, "APP_PROFILE"];
+ } else {
+ $this->configKey = $configKey;
+ $this->envKeys = [$envKey];
+ }
+ }
+ $this->defaultProfile = $params["default_profile"] ?? null;
+ $profile = $params["profile"] ?? null;
+ $productionMode = $params["production_mode"] ?? null;
+ $productionMode ??= $this->productionModes[$profile] ?? false;
+ $this->profile = $profile;
+ $this->productionMode = $productionMode;
+ }
+
+ /**
+ * @var bool cet objet est-il utilisé pour gérer le profil de l'application?
+ */
+ protected bool $isAppProfile;
+
+ protected ?array $profiles;
+
+ protected ?array $productionModes;
+
+ protected ?array $profileMap;
+
+ protected function mapProfile(?string $profile): ?string {
+ return $this->profileMap[$profile] ?? $profile;
+ }
+
+ protected ?string $configKey;
+
+ function getConfigProfile(): ?string {
+ if ($this->configKey === null) return null;
+ return config::k($this->configKey);
+ }
+
+ protected array $envKeys;
+
+ function getEnvProfile(): ?string {
+ foreach ($this->envKeys as $envKey) {
+ $profile = getenv($envKey);
+ if ($profile !== false) return $profile;
+ }
+ return null;
+ }
+
+ protected ?string $defaultProfile;
+
+ function getDefaultProfile(): ?string {
+ return $this->defaultProfile;
+ }
+
+ function setDefaultProfile(?string $profile): void {
+ $this->defaultProfile = $profile;
+ }
+
+ protected ?string $profile;
+
+ protected bool $productionMode;
+
+ protected function resolveProfile(): void {
+ $profile ??= $this->getenvProfile();
+ $profile ??= $this->getConfigProfile();
+ $profile ??= $this->getDefaultProfile();
+ if ($this->isAppProfile) {
+ $profile ??= $this->profiles[0] ?? "prod";
+ } else {
+ $profile ??= $this->mapProfile(app::get_profile());
+ }
+ $this->profile = $profile;
+ $this->productionMode = $this->productionModes[$profile] ?? false;
+ }
+
+ function getProfile(?bool &$productionMode=null): string {
+ if ($this->profile === null) $this->resolveProfile();
+ $productionMode = $this->productionMode;
+ return $this->profile;
+ }
+
+ function isProductionMode(): bool {
+ return $this->productionMode;
+ }
+
+ function setProfile(?string $profile=null, ?bool $productionMode=null): void {
+ if ($profile === null) $this->profile = null;
+ $profile ??= $this->getProfile($productionMode);
+ $productionMode ??= $this->productionModes[$profile] ?? false;
+ $this->profile = $profile;
+ $this->productionMode = $productionMode;
+ }
+}
diff --git a/php/src/app/config/YamlConfig.php b/php/src/app/config/YamlConfig.php
new file mode 100644
index 0000000..e248c9c
--- /dev/null
+++ b/php/src/app/config/YamlConfig.php
@@ -0,0 +1,13 @@
+ $projdir,
+ "vendor" => [
+ "bindir" => "$projdir/vendor/bin",
+ "autoload" => "$projdir/vendor/autoload.php",
+ ],
+ "appcode" => "nur-ture",
+ "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-ture",
+ "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-ture",
+ "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-ture",
+ "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\app\impl {
+
+ use nulib\app\cli\Application;
+ use nulib\os\path;
+ use nulib\app\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/app/cli/AodefTest.php b/php/tests/app/cli/AodefTest.php
new file mode 100644
index 0000000..7d1c828
--- /dev/null
+++ b/php/tests/app/cli/AodefTest.php
@@ -0,0 +1,159 @@
+setup1();
+ $aodef->setup2();
+ #var_export($aodef->debugInfos()); #XXX
+ self::assertSame($options, $aodef->getOptions());
+ self::assertSame($haveShortOptions, $aodef->haveShortOptions, "haveShortOptions");
+ self::assertSame($haveLongOptions, $aodef->haveLongOptions, "haveLongOptions");
+ self::assertSame($isCommand, $aodef->isCommand, "isCommand");
+ self::assertSame($haveArgs, $aodef->haveArgs, "haveArgs");
+ self::assertSame($minArgs, $aodef->minArgs, "minArgs");
+ self::assertSame($maxArgs, $aodef->maxArgs, "maxArgs");
+ self::assertSame($argsdesc, $aodef->argsdesc, "argsdesc");
+ }
+
+ function testArgsNone() {
+ $aodef = new Aodef(["-o"]);
+ self::assertArg($aodef,
+ ["-o"],
+ true, false, false,
+ false, 0, 0, "");
+
+ $aodef = new Aodef(["--longo"]);
+ self::assertArg($aodef,
+ ["--longo"],
+ false, true, false,
+ false, 0, 0, "");
+
+ $aodef = new Aodef(["-o", "--longo"]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ false, 0, 0, "");
+ }
+
+ function testArgsMandatory() {
+ $aodef = new Aodef(["-o:", "--longo"]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 1, 1, "VALUE");
+
+ $aodef = new Aodef(["-a:", "-b:"]);
+ self::assertArg($aodef,
+ ["-a", "-b"],
+ true, false, false,
+ true, 1, 1, "VALUE");
+
+ $aodef = new Aodef(["-a:", "-b::"]);
+ self::assertArg($aodef,
+ ["-a", "-b"],
+ true, false, false,
+ true, 1, 1, "VALUE");
+
+ $aodef = new Aodef(["-a::", "-b:"]);
+ self::assertArg($aodef,
+ ["-a", "-b"],
+ true, false, false,
+ true, 1, 1, "VALUE");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => true]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 1, 1, "VALUE");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => 1]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 1, 1, "VALUE");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => "value"]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 1, 1, "VALUE");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => ["value"]]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 1, 1, "VALUE");
+ }
+
+ function testArgsOptional() {
+ $aodef = new Aodef(["-o::", "--longo"]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 0, 1, "[VALUE]");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => [["value"]]]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 0, 1, "[VALUE]");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => [[null]]]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 0, PHP_INT_MAX, "[VALUEs...]");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => ["value", null]]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 1, PHP_INT_MAX, "VALUE [VALUEs...]");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => "*"]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 0, PHP_INT_MAX, "[VALUEs...]");
+
+ $aodef = new Aodef(["-o", "--longo", "args" => "+"]);
+ self::assertArg($aodef,
+ ["-o", "--longo"],
+ true, true, false,
+ true, 1, PHP_INT_MAX, "VALUE [VALUEs...]");
+ }
+
+ function testMerge() {
+ $BASE = ["-o:", "--longo"];
+
+ $aodef = new Aodef([
+ "merge" => $BASE,
+ "add" => ["-a", "--longa"],
+ "remove" => ["-o", "--longo"],
+ ]);
+ self::assertArg($aodef,
+ ["-a", "--longa"],
+ true, true, false,
+ false, 0, 0, "");
+
+ $aodef = new Aodef([
+ "merge" => $BASE,
+ "add" => ["-a", "--longa"],
+ "remove" => ["-o", "--longo"],
+ "-x",
+ ]);
+ self::assertArg($aodef,
+ ["-a", "--longa", "-x"],
+ true, true, false,
+ false, 0, 0, "");
+ }
+}
diff --git a/php/tests/app/cli/AolistTest.php b/php/tests/app/cli/AolistTest.php
new file mode 100644
index 0000000..4b60541
--- /dev/null
+++ b/php/tests/app/cli/AolistTest.php
@@ -0,0 +1,63 @@
+ "value",
+ ["--opt"],
+ ["group",
+ ["--gopt1"],
+ ["--gopt2"],
+ ],
+ "sections" => [
+ [
+ ["--s0opt"],
+ ["group",
+ ["--s0gopt1"],
+ ["--s0gopt2"],
+ ],
+ ],
+ "ns" => [
+ ["--nsopt"],
+ ["group",
+ ["--nsgopt1"],
+ ["--nsgopt2"],
+ ],
+ ],
+ ],
+ ]) extends Aolist {};
+
+ echo "$aolist\n";
+ self::assertTrue(true);
+ }
+}
diff --git a/php/tests/app/cli/SimpleAolistTest.php b/php/tests/app/cli/SimpleAolistTest.php
new file mode 100644
index 0000000..e216f8d
--- /dev/null
+++ b/php/tests/app/cli/SimpleAolistTest.php
@@ -0,0 +1,59 @@
+ [
+ ["-o", "--longo"],
+ ],
+ ]);
+ echo "$aolist\n"; #XXX
+
+ $aolist = new SimpleAolist([
+ ["-o", "--longo"],
+ ["-o", "--longx"],
+ ]);
+ echo "$aolist\n"; #XXX
+
+ $aolist = new SimpleAolist([
+ ["-o", "--longo"],
+ ["-o"],
+ ["--longo"],
+ ]);
+ echo "$aolist\n"; #XXX
+
+ self::assertTrue(true);
+ }
+
+ function testExtends() {
+ $ARGS0 = [
+ ["-o:", "--longo",
+ "name" => "desto",
+ "help" => "help longo"
+ ],
+ ["-a:", "--longa",
+ "name" => "desta",
+ "help" => "help longa"
+ ],
+ ];
+ $ARGS = [
+ "merge" => $ARGS0,
+ ["extends" => "-a",
+ "remove" => ["--longa"],
+ "add" => ["--desta"],
+ "help" => "help desta"
+ ],
+ ];
+ //$aolist0 = new SimpleArgDefs($ARGS0);
+ //echo "$aolist0\n"; #XXX
+ $aolist = new SimpleAolist($ARGS);
+ echo "$aolist\n"; #XXX
+
+ self::assertTrue(true);
+ }
+}
diff --git a/php/tests/app/cli/SimpleArgsParserTest.php b/php/tests/app/cli/SimpleArgsParserTest.php
new file mode 100644
index 0000000..6dd73e7
--- /dev/null
+++ b/php/tests/app/cli/SimpleArgsParserTest.php
@@ -0,0 +1,175 @@
+ [["value", "value"]]],
+ ["--mo12:", "args" => ["value", ["value"]]],
+ ["--mo22:", "args" => ["value", "value"]],
+ ];
+ const NORMALIZE_TESTS = [
+ [], ["--"],
+ ["--"], ["--"],
+ ["--", "--"], ["--", "--"],
+ ["-aa"], ["-a", "-a", "--"],
+ ["a", "b"], ["--", "a", "b"],
+ ["-a", "--ma", "x", "a", "--ma=y", "b"], ["-a", "--mandatory", "x", "--mandatory", "y", "--", "a", "b"],
+ ["-mx", "-m", "y"], ["-m", "x", "-m", "y", "--"],
+ ["-ox", "-o", "y"], ["-ox", "-o", "--", "y"],
+ ["-a", "--", "-a", "-c"], ["-a", "--", "-a", "-c"],
+
+ # -a et -b doivent être considérés comme arguments, -n comme option
+ ["--mo02"], ["--mo02", "--", "--"],
+ ["--mo02", "-a"], ["--mo02", "-a", "--", "--"],
+ ["--mo02", "--"], ["--mo02", "--", "--"],
+ ["--mo02", "--", "-n"], ["--mo02", "--", "-n", "--"],
+ ["--mo02", "--", "--", "-b"], ["--mo02", "--", "--", "-b"],
+ #
+ ["--mo02", "-a"], ["--mo02", "-a", "--", "--"],
+ ["--mo02", "-a", "-a"], ["--mo02", "-a", "-a", "--"],
+ ["--mo02", "-a", "--"], ["--mo02", "-a", "--", "--"],
+ ["--mo02", "-a", "--", "-n"], ["--mo02", "-a", "--", "-n", "--"],
+ ["--mo02", "-a", "--", "--", "-b"], ["--mo02", "-a", "--", "--", "-b"],
+
+ [
+ "--mo02", "--",
+ "--mo02", "x", "--",
+ "--mo02", "x", "y",
+ "--mo12", "x", "--",
+ "--mo12", "x", "y",
+ "--mo22", "x", "y",
+ "z",
+ ], [
+ "--mo02", "--",
+ "--mo02", "x", "--",
+ "--mo02", "x", "y",
+ "--mo12", "x", "--",
+ "--mo12", "x", "y",
+ "--mo22", "x", "y",
+ "--",
+ "z",
+ ],
+ ];
+
+ function testNormalize() {
+ $parser = new SimpleArgsParser(self::NORMALIZE_ARGS);
+ $count = count(self::NORMALIZE_TESTS);
+ for ($i = 0; $i < $count; $i += 2) {
+ $args = self::NORMALIZE_TESTS[$i];
+ $expected = self::NORMALIZE_TESTS[$i + 1];
+ $normalized = $parser->normalize($args);
+ self::assertSame($expected, $normalized
+ , "for ".var_export($args, true)
+ .", normalized is ".var_export($normalized, true)
+ );
+ }
+ }
+
+ function testArgsNone() {
+ $parser = new SimpleArgsParser([
+ ["-z"],
+ ["-a"],
+ ["-b"],
+ ["-c",],
+ ["-d", "value" => 42],
+ ]);
+
+ $dest = []; $parser->parse($dest, ["-a", "-bb", "-ccc", "-dddd"]);
+ self::assertSame(null, $dest["z"] ?? null);
+ self::assertSame(1, $dest["a"] ?? null);
+ self::assertSame(2, $dest["b"] ?? null);
+ self::assertSame(3, $dest["c"] ?? null);
+ self::assertSame(42, $dest["d"] ?? null);
+
+ self::assertTrue(true);
+ }
+
+ function testArgsMandatory() {
+ $parser = new SimpleArgsParser([
+ ["-z:"],
+ ["-a:"],
+ ["-b:"],
+ ["-c:", "value" => 42],
+ ]);
+
+ $dest = []; $parser->parse($dest, [
+ "-a",
+ "-bb",
+ "-c",
+ "-c15",
+ "-c30",
+ "-c45",
+ ]);
+ self::assertSame(null, $dest["z"] ?? null);
+ self::assertSame("-bb", $dest["a"] ?? null);
+ self::assertSame(null, $dest["b"] ?? null);
+ self::assertSame("45", $dest["c"] ?? null);
+
+ self::assertTrue(true);
+ }
+
+ function testArgsOptional() {
+ $parser = new SimpleArgsParser([
+ ["-z::"],
+ ["-a::"],
+ ["-b::"],
+ ["-c::", "value" => 42],
+ ["-d::", "value" => 42],
+ ]);
+
+ $dest = []; $parser->parse($dest, [
+ "-a",
+ "-bb",
+ "-c",
+ "-d15",
+ "-d30",
+ ]);
+ self::assertSame(null, $dest["z"] ?? null);
+ self::assertSame(null, $dest["a"] ?? null);
+ self::assertSame("b", $dest["b"] ?? null);
+ self::assertSame(42, $dest["c"] ?? null);
+ self::assertSame("30", $dest["d"] ?? null);
+
+ self::assertTrue(true);
+ }
+
+ function testRemains() {
+ $parser = new SimpleArgsParser([]);
+ $dest = []; $parser->parse($dest, ["x", "y"]);
+ self::assertSame(["x", "y"], $dest["args"] ?? null);
+ }
+
+ function test() {
+ $parser = new SimpleArgsParser([
+ ["-n", "--none"],
+ ["-m:", "--mandatory"],
+ ["-o::", "--optional"],
+ ["--mo02:", "args" => [["value", "value"]]],
+ ["--mo12:", "args" => ["value", ["value"]]],
+ ["--mo22:", "args" => ["value", "value"]],
+ ]);
+ $parser->parse($dest, [
+ "--mo02", "--",
+ "--mo02", "x", "--",
+ "--mo02", "x", "y",
+ "--mo12", "x", "--",
+ "--mo12", "x", "y",
+ "--mo22", "x", "y",
+ "z",
+ ]);
+
+ self::assertTrue(true);
+ }
+}
diff --git a/php/tests/app/config/ConfigManagerTest.php b/php/tests/app/config/ConfigManagerTest.php
new file mode 100644
index 0000000..69ba5f8
--- /dev/null
+++ b/php/tests/app/config/ConfigManagerTest.php
@@ -0,0 +1,124 @@
+addConfigurator(config1::class);
+ $config->configure();
+ self::assertSame([
+ "config1::static configure1",
+ ], impl\result::$configured);
+
+ result::reset();
+ $config->addConfigurator(config1::class);
+ $config->configure();
+ $config->configure();
+ $config->configure();
+ self::assertSame([
+ "config1::static configure1",
+ ], impl\result::$configured);
+
+ result::reset();
+ $config->addConfigurator(new config1());
+ $config->configure();
+ self::assertSame([
+ "config1::static configure1",
+ "config1::configure2",
+ ], impl\result::$configured);
+
+ result::reset();
+ $config->addConfigurator(new config1());
+ $config->configure(["include" => "2"]);
+ self::assertSame([
+ "config1::configure2",
+ ], impl\result::$configured);
+ $config->configure(["include" => "1"]);
+ self::assertSame([
+ "config1::configure2",
+ "config1::static configure1",
+ ], impl\result::$configured);
+
+ result::reset();
+ $config->addConfigurator([
+ config1::class,
+ new config2(),
+ ]);
+ $config->configure();
+ self::assertSame([
+ "config1::static configure1",
+ "config2::static configure1",
+ "config2::configure2",
+ ], impl\result::$configured);
+ }
+
+ function testConfig() {
+ $config = new ConfigManager();
+
+ $config->addConfig([
+ "app" => [
+ "var" => "array",
+ ]
+ ]);
+ self::assertSame("array", $config->getValue("app.var"));
+
+ $config->addConfig(new ArrayConfig([
+ "app" => [
+ "var" => "instance",
+ ]
+ ]));
+ self::assertSame("instance", $config->getValue("app.var"));
+
+ $config->addConfig(config1::class);
+ self::assertSame("class1", $config->getValue("app.var"));
+
+ $config->addConfig(config2::class);
+ self::assertSame("class2", $config->getValue("app.var"));
+ }
+ }
+}
+
+namespace nulib\app\config\impl {
+ class result {
+ static array $configured = [];
+
+ static function reset() {
+ self::$configured = [];
+ }
+ }
+
+ class config1 {
+ const APP = [
+ "var" => "class1",
+ ];
+
+ static function configure1() {
+ result::$configured[] = "config1::static configure1";
+ }
+
+ function configure2() {
+ result::$configured[] = "config1::configure2";
+ }
+ }
+
+ class config2 {
+ const APP = [
+ "var" => "class2",
+ ];
+
+ static function configure1() {
+ result::$configured[] = "config2::static configure1";
+ }
+
+ function configure2() {
+ result::$configured[] = "config2::configure2";
+ }
+ }
+}