migration d'outils depuis nur-ture

This commit is contained in:
Jephté Clain 2025-10-03 01:44:49 +04:00
parent 6a41b72e95
commit efb7901498
70 changed files with 4687 additions and 27 deletions

1
.idea/nulib-base.iml generated
View File

@ -4,6 +4,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/php/src" isTestSource="false" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/php/tests" isTestSource="true" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/php/cli" isTestSource="false" packagePrefix="cli\" />
<excludeFolder url="file://$MODULE_DIR$/php/vendor" />
</content>
<orderEntry type="inheritedJdk" />

View File

@ -2,9 +2,7 @@
<?php
require __DIR__ . "/../php/vendor/autoload.php";
use nulib\tools\pman\ComposerFile;
use nulib\tools\pman\ComposerPmanFile;
use nulib\ValueException;
use cli\pman\ComposerFile;
$composer = new ComposerFile();

View File

@ -2,8 +2,8 @@
<?php
require __DIR__ . "/../php/vendor/autoload.php";
use nulib\tools\pman\ComposerFile;
use nulib\tools\pman\ComposerPmanFile;
use cli\pman\ComposerFile;
use cli\pman\ComposerPmanFile;
use nulib\ValueException;
$composer = new ComposerFile();

1
bin/.dumpser.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/dumpser.php

1
bin/.json2yml.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/json2yml.php

1
bin/.mysql.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/mysql.capacitor.php

1
bin/.pgsql.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/pgsql.capacitor.php

1
bin/.sqlite.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/sqlite.capacitor.php

1
bin/.yml2json.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/yml2json.php

1
bin/dumpser.php Symbolic link
View File

@ -0,0 +1 @@
runphp

1
bin/json2yml.php Symbolic link
View File

@ -0,0 +1 @@
runphp

1
bin/mysql.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
runphp

1
bin/pgsql.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
runphp

1
bin/sqlite.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
runphp

1
bin/yml2json.php Symbolic link
View File

@ -0,0 +1 @@
runphp

View File

@ -38,7 +38,8 @@
},
"autoload": {
"psr-4": {
"nulib\\": "php/src"
"nulib\\": "php/src",
"cli\\": "php/cli"
}
},
"autoload-dev": {
@ -46,6 +47,14 @@
"nulib\\": "php/tests"
}
},
"bin": [
"php/bin/dumpser.php",
"php/bin/json2yml.php",
"php/bin/yml2json.php",
"php/bin/sqlite.capacitor.php",
"php/bin/mysql.capacitor.php",
"php/bin/pgsql.capacitor.php"
],
"config": {
"vendor-dir": "php/vendor"
},

51
composer.lock generated
View File

@ -420,16 +420,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.10.0",
"version": "v6.11.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284",
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284",
"shasum": ""
},
"require": {
@ -450,6 +450,7 @@
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
@ -489,7 +490,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.11.1"
},
"funding": [
{
@ -497,7 +498,7 @@
"type": "github"
}
],
"time": "2025-04-24T15:19:31+00:00"
"time": "2025-09-30T11:54:53+00:00"
},
{
"name": "psr/cache",
@ -2147,16 +2148,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.25",
"version": "9.6.29",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "049c011e01be805202d8eebedef49f769a8ec7b7"
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/049c011e01be805202d8eebedef49f769a8ec7b7",
"reference": "049c011e01be805202d8eebedef49f769a8ec7b7",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
"shasum": ""
},
"require": {
@ -2181,7 +2182,7 @@
"sebastian/comparator": "^4.0.9",
"sebastian/diff": "^4.0.6",
"sebastian/environment": "^5.1.5",
"sebastian/exporter": "^4.0.6",
"sebastian/exporter": "^4.0.8",
"sebastian/global-state": "^5.0.8",
"sebastian/object-enumerator": "^4.0.4",
"sebastian/resource-operations": "^3.0.4",
@ -2230,7 +2231,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.25"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29"
},
"funding": [
{
@ -2254,7 +2255,7 @@
"type": "tidelift"
}
],
"time": "2025-08-20T14:38:31+00:00"
"time": "2025-09-24T06:29:11+00:00"
},
{
"name": "sebastian/cli-parser",
@ -2697,16 +2698,16 @@
},
{
"name": "sebastian/exporter",
"version": "4.0.6",
"version": "4.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
"reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
"reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c",
"reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c",
"shasum": ""
},
"require": {
@ -2762,15 +2763,27 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
"type": "tidelift"
}
],
"time": "2024-03-02T06:33:00+00:00"
"time": "2025-09-24T06:03:27+00:00"
},
{
"name": "sebastian/global-state",

7
php/bin/dumpser.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\DumpserApp;
DumpserApp::run();

7
php/bin/json2yml.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\Json2yamlApp;
Json2yamlApp::run();

7
php/bin/mysql.capacitor.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\MysqlCapacitorApp;
MysqlCapacitorApp::run();

7
php/bin/pgsql.capacitor.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\PgsqlCapacitorApp;
PgsqlCapacitorApp::run();

7
php/bin/sqlite.capacitor.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\SqliteCapacitorApp;
SqliteCapacitorApp::run();

7
php/bin/yml2json.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\Yaml2jsonApp;
Yaml2jsonApp::run();

View File

@ -0,0 +1,111 @@
<?php
namespace cli;
use nulib\A;
use nulib\app\cli\Application;
use nulib\db\Capacitor;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
use nulib\ext\yaml;
use nulib\file\Stream;
use nulib\output\msg;
abstract class AbstractCapacitorApp extends Application {
const ACTION_RESET = 0, ACTION_QUERY = 1, ACTION_SQL = 2;
protected ?string $tableName = null;
protected ?string $channelClass = null;
protected int $action = self::ACTION_QUERY;
protected bool $recreate = true;
protected static function isa_cond(string $arg, ?array &$ms=null): bool {
return preg_match('/^(.+?)\s*(=|<>|<|>|<=|>=|(?: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;
}
}
}

122
php/cli/BgLauncherApp.php Normal file
View File

@ -0,0 +1,122 @@
<?php
namespace cli;
use nulib\app\app;
use nulib\app\cli\Application;
use nulib\app\RunFile;
use nulib\ExitError;
use nulib\ext\yaml;
use nulib\os\path;
use nulib\os\proc\Cmd;
use nulib\os\sh;
use nulib\output\msg;
class BgLauncherApp extends Application {
const ACTION_INFOS = 0, ACTION_START = 1, ACTION_STOP = 2;
const ARGS = [
"purpose" => "lancer un script en tâche de fond",
"usage" => "ApplicationClass args...",
"sections" => [
parent::VERBOSITY_SECTION,
],
["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS,
"help" => "Afficher des informations sur la tâche",
],
["-s", "--start", "name" => "action", "value" => self::ACTION_START,
"help" => "Démarrer la tâche",
],
["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP,
"help" => "Arrêter la tâche",
],
];
protected int $action = self::ACTION_START;
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;
}
}
}

31
php/cli/DumpserApp.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace cli;
use nulib\app\cli\Application;
use nulib\ext\yaml;
use nulib\file\SharedFile;
use nulib\output\msg;
class DumpserApp extends Application {
const ARGS = [
"merge" => 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());
}
}
}

21
php/cli/Json2yamlApp.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace cli;
use nulib\app\cli\Application;
use nulib\ext\json;
use nulib\ext\yaml;
use nulib\os\path;
class Json2yamlApp extends Application {
function main() {
$input = $this->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);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace cli;
use nulib\A;
use nulib\app\config;
use nulib\db\mysql\MysqlStorage;
class MysqlCapacitorApp extends AbstractCapacitorApp {
const ARGS = [
"merge" => 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);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace cli;
use nulib\A;
use nulib\app\config;
use nulib\db\pgsql\PgsqlStorage;
class PgsqlCapacitorApp extends AbstractCapacitorApp {
const ARGS = [
"merge" => 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);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace cli;
use nulib\A;
use nulib\db\sqlite\SqliteStorage;
class SqliteCapacitorApp extends AbstractCapacitorApp {
const ARGS = [
"merge" => 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);
}
}

53
php/cli/SteamTrainApp.php Normal file
View File

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

21
php/cli/Yaml2jsonApp.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace cli;
use nulib\app\cli\Application;
use nulib\ext\json;
use nulib\ext\yaml;
use nulib\os\path;
class Yaml2jsonApp extends Application {
function main() {
$input = $this->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);
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\tools\pman;
namespace cli\pman;
use nulib\cl;
use nulib\ext\json;

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\tools\pman;
namespace cli\pman;
use nulib\A;
use nulib\ext\yaml;

614
php/src/app/app.php Normal file
View File

@ -0,0 +1,614 @@
<?php
namespace nulib\app;
use nulib\A;
use nulib\app\cli\Application;
use nulib\app\config\ProfileManager;
use nulib\cl;
use nulib\ExitError;
use nulib\os\path;
use nulib\os\sh;
use nulib\php\func;
use nulib\str;
use nulib\ValueException;
class app {
private static function isa_Application($app): bool {
if (!is_string($app)) return false;
return $app === Application::class
|| is_subclass_of($app, Application::class);
}
private static function get_params($app): array {
if ($app instanceof self) {
$params = $app->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);
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace nulib\app\args;
use nulib\app\args\ArgsException;
use stdClass;
abstract class AbstractArgsParser {
protected function notEnoughArgs(int $needed, ?string $arg=null): ArgsException {
if ($arg !== null) $arg .= ": ";
return new ArgsException("${arg}nécessite $needed argument(s) supplémentaires");
}
protected function checkEnoughArgs(?string $option, int $count): void {
if ($count > 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;
}

623
php/src/app/args/Aodef.php Normal file
View File

@ -0,0 +1,623 @@
<?php
namespace nulib\app\args;
use nulib\A;
use nulib\app\args\AbstractArgsParser;
use nulib\app\args\Aolist;
use nulib\app\args\ArgsException;
use nulib\cl;
use nulib\php\akey;
use nulib\php\func;
use nulib\php\oprop;
use nulib\php\types\varray;
use nulib\php\types\vbool;
use nulib\php\valx;
use nulib\str;
/**
* Class Aodef: une définition d'un argument
*
* il y a 3 temps dans l'initialisation de l'objet:
* - constructeur: accumuler les informations
* - setup1($extends): calculer les options effectives. $extends permet de
* cibler les définitions qui étendent une définition existante
* - setup2(): calculer les arguments et les actions
*/
class Aodef {
const TYPE_SHORT = 0, TYPE_LONG = 1, TYPE_COMMAND = 2;
const ARGS_NONE = 0, ARGS_MANDATORY = 1, ARGS_OPTIONAL = 2;
function __construct(array $def) {
$this->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";
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace nulib\app\args;
use nulib\A;
use nulib\app\args\Aolist;
use nulib\app\args\ArgsException;
/**
* Class Aogroup: groupe d'arguments fonctionnant ensemble
*/
class Aogroup extends Aolist {
function __construct(array $defs, bool $setup=false) {
$marker = A::pop($defs, 0);
if ($marker !== "group") {
throw ArgsException::invalid(null, "group");
}
# réordonner les clés numériques
$defs = array_merge($defs);
parent::__construct($defs, $setup);
}
function printHelp(?array $what=null): void {
$showGroup = $what["show"] ?? true;
if (!$showGroup) return;
$prefix = $what["prefix"] ?? null;
if ($prefix !== null) echo $prefix;
$firstAodef = null;
foreach ($this->all() as $aodef) {
$firstAodef ??= $aodef;
$aodef->printHelp(["help" => false]);
}
if ($firstAodef !== null) {
$firstAodef->printHelp(["options" => false]);
}
}
}

271
php/src/app/args/Aolist.php Normal file
View File

@ -0,0 +1,271 @@
<?php
namespace nulib\app\args;
use nulib\app\args\Aodef;
use nulib\app\args\Aogroup;
use nulib\app\args\Aosection;
use nulib\cl;
use nulib\str;
use const true;
/**
* Class Aodefs: une liste d'objets Aodef
*/
abstract class Aolist {
function __construct(array $defs, bool $setup=true) {
$this->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);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace nulib\app\args;
use nulib\app\args\Aodef;
use nulib\app\args\Aolist;
use nulib\php\types\vbool;
/**
* Class Aosection: un regroupement d'arguments pour améliorer la mise en forme
* de l'affichage de l'aide
*/
class Aosection extends Aolist {
function __construct(array $defs, bool $setup=false) {
parent::__construct($defs, $setup);
}
public bool $show = true;
public ?string $prefix = null;
public ?string $title = null;
public ?string $description = null;
public ?string $suffix = null;
protected function parseParams(?array $params): void {
$this->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";
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace nulib\app\args;
use nulib\ValueException;
class ArgsException extends ValueException {
static function missing(?string $value, string $kind): self {
$msg = $value;
if ($msg !== null) $msg .= ": ";
$msg .= "missing $kind";
throw new self($msg);
}
static function invalid(?string $value, string $kind): self {
$msg = $value;
if ($msg !== null) $msg .= ": ";
$msg .= "invalid $kind";
throw new self($msg);
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace nulib\app\args;
use nulib\app\args\Aodef;
use nulib\app\args\Aolist;
use nulib\cl;
use nulib\php\types\vbool;
use nulib\str;
use const true;
/**
* Class SimpleArgdefs: une définition simple des arguments et des options
* valides d'un programme: les commandes ne sont pas supportées, ni les suites
* de commandes
*
* i.e
* -x --long est supporté
* cmd -a -b n'est PAS supporté
* cmd1 -x // cmd2 -y n'est PAS supporté
*/
class SimpleAolist extends Aolist {
public ?string $prefix = null;
public ?string $name = null;
public ?string $purpose = null;
public $usage = null;
public ?string $description = null;
public ?string $suffix = null;
public ?string $commandname = null;
public ?string $commandproperty = null;
public ?string $commandkey = null;
public ?string $argsname = null;
public ?string $argsproperty = null;
public ?string $argskey = null;
public ?bool $autohelp = null;
public ?bool $autoremains = null;
protected array $index;
protected function parseParams(?array $params): void {
# méta-informations
$this->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))),
]);
}
}

View File

@ -0,0 +1,250 @@
<?php
namespace nulib\app\args;
use nulib\app\args\AbstractArgsParser;
use nulib\app\args\Aodef;
use nulib\app\args\SimpleAolist;
use nulib\cl;
use nulib\ExitError;
use nulib\StateException;
class SimpleArgsParser extends AbstractArgsParser {
function __construct(array $defs) {
global $argv;
$defs["name"] ??= basename($argv[0]);
$this->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
}
}

21
php/src/app/args/TODO.md Normal file
View File

@ -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

View File

@ -0,0 +1,383 @@
<?php
namespace nulib\app\cli;
use Exception;
use nulib\app\app;
use nulib\app\args\AbstractArgsParser;
use nulib\app\args\ArgsException;
use nulib\app\args\SimpleArgsParser;
use nulib\app\config;
use nulib\app\RunFile;
use nulib\ExitError;
use nulib\ext\yaml;
use nulib\output\console;
use nulib\output\log;
use nulib\output\msg;
use nulib\output\std\StdMessenger;
use nulib\ValueException;
/**
* Class Application: application de base
*/
abstract class Application {
/** @var string répertoire du projet (celui qui contient composer.json */
const PROJDIR = null;
/**
* @var array répertoires vendor exprimés relativement à PROJDIR
*
* les clés suivantes doivent être présentes dans le tableau:
* - autoload (chemin vers vendor/autoload.php)
* - bindir (chemin vers vendor/bin)
*/
const VENDOR = null;
/**
* @var string code du projet, utilisé pour dériver le noms de certains des
* paramètres extraits de l'environnement, e.g XXX_YYY_DATADIR si le projet a
* pour code xxx-yyy
*
* si non définie, cette valeur est calculée automatiquement à partir de
* self::PROJDIR sans le suffixe "-app"
*/
const APPCODE = null;
/**
* @var string|null identifiant d'un groupe auquel l'application appartient.
* les applications du même groupe enregistrent leur fichiers de controle au
* même endroit $VARDIR/$APPGROUP
*/
const APPGROUP = null;
/**
* @var string code de l'application, utilisé pour inférer le nom de certains
* fichiers spécifiques à l'application.
*
* si non définie, cette valeur est calculée automatiquement à partir de
* static::class
*/
const NAME = null;
/** @var string description courte de l'application */
const TITLE = null;
const DATADIR = null;
const ETCDIR = null;
const VARDIR = null;
const LOGDIR = null;
/** @var bool faut-il activer automatiquement l'écriture dans les logs */
const USE_LOGFILE = null;
/** @var bool faut-il maintenir un fichier de suivi du process? */
const USE_RUNFILE = false;
/**
* @var bool faut-il empêcher deux instances de cette application de se lancer
* en même temps?
*
* nécessite USE_RUNFILE==true
*/
const USE_RUNLOCK = false;
/** @var bool faut-il installer le gestionnaire de signaux? */
const INSTALL_SIGNAL_HANDLER = false;
private static function _info(string $message, int $ec=0): int {
fwrite(STDERR, "INFO: $message\n");
return $ec;
}
private static function _error(string $message, int $ec=1): int {
fwrite(STDERR, "ERROR: $message\n");
return $ec;
}
static function _manage_runfile(int &$argc, array &$argv, RunFile $runfile): void {
if ($argc <= 1 || $argv[1] !== "//") return;
array_splice($argv, 1, 1); $argc--;
$ec = 0;
switch ($argv[1] ?? "infos") {
case "help":
self::_info(<<<EOT
Valid commands:
infos
dump
reset
release
start
kill
EOT);
break;
case "infos":
case "i":
$desc = $runfile->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? "<color $color>$profile</color>": $profile;
}
}
$color = static::DEFAULT_PROFILE_COLOR;
return $color? "<color $color>$profile</color>": $profile;
}
protected ?array $args = null;
abstract function main();
static function runfile(): RunFile {
return app::with(static::class)->getRunfile();
}
}

41
php/src/app/config.php Normal file
View File

@ -0,0 +1,41 @@
<?php
namespace nulib\app;
use nulib\app\config\ConfigManager;
use nulib\cl;
/**
* Class config: gestion de la configuration de l'application
*/
class config {
protected static ConfigManager $config;
static function init_configurator($configurators): void {
self::$config->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();
}
};

View File

@ -0,0 +1,50 @@
<?php
namespace nulib\app\config;
use nulib\cl;
class ArrayConfig implements IConfig {
protected function APP(): array {
return static::APP;
} const APP = [];
protected function DBS(): array {
return static::DBS;
} const DBS = [];
protected function MSGS(): array {
return static::MSGS;
} const MSGS = [];
protected function MAILS(): array {
return static::MAILS;
} const MAILS = [];
function __construct(?array $config) {
foreach (self::CONFIG_KEYS as $key) {
switch ($key) {
case "app": $default = $this->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);
}
}

View File

@ -0,0 +1,150 @@
<?php
namespace nulib\app\config;
use nulib\A;
use nulib\app\app;
use nulib\cl;
use nulib\php\func;
use nulib\ValueException;
use ReflectionClass;
class ConfigManager {
protected array $configurators = [];
/** ajouter une classe ou un objet à la liste des configurateurs */
function addConfigurator($configurators): void {
A::merge($this->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);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace nulib\app\config;
use nulib\cl;
use nulib\ext\json;
use nulib\file;
use nulib\str;
/**
* Class EnvConfig: configuration extraite depuis les variables d'environnement
*
* les variables doivent être de la forme {PREFIX}_{PROFILE}_{PKEY} :
* - PREFIX vaut CONFIG, FILE_CONFIG, JSON_CONFIG ou JSON_FILE_CONFIG
* - PROFILE est le profil dans lequel la configuration est valide, ou ALL si
* la valeur doit être valide dans tous les profils
* - PKEY est le chemin de clé dans lequel les caractères '.' sont remplacés
* par '__'
*
* par exemple, la valeur dbs.my_auth.type du profil par défaut est pris dans
* la variable 'CONFIG_ALL_dbs__my_auth__type'. pour le profil prod c'est la
* variable 'CONFIG_prod_dbs__my_auth__type'
*
* pour représenter le tableau suivant:
* [ "type" => "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);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace nulib\app\config;
/**
* Interface IConfig: un objet fournissant une configuration
*/
interface IConfig {
/**
* @var string profil indiquant qu'une configuration est valide dans tous les
* profils
*/
const PROFILE_ALL = "-ALL-";
const CONFIG_KEYS = [
"app",
"dbs", "msgs", "mails",
];
function has(string $pkey, string $profile): bool;
function get(string $pkey, string $profile);
function set(string $pkey, $value, string $profile): void;
}

View File

@ -0,0 +1,13 @@
<?php
namespace nulib\app\config;
use nulib\ext\json;
/**
* Class JsonConfig: une configuration chargée depuis un fichier JSON
*/
class JsonConfig extends ArrayConfig {
function __construct(string $input) {
parent::__construct(json::load($input));
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace nulib\app\config;
use nulib\app\app;
use nulib\app\config;
/**
* Class ProfileManager: gestionnaire de profils
*/
class ProfileManager {
/**
* @var string code du système dont on gère le profil
*
* ce code est utilisé pour dériver le nom du paramètre dans la configuration
* ainsi que la variable d'environnement depuis laquelle est chargée la valeur
* du profil
*/
const NAME = null;
/** @var array|null liste des profils valides */
const PROFILES = null;
/** @var array profils dont le mode production doit être actif */
const PRODUCTION_MODES = [
"prod" => 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;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace nulib\app\config;
use nulib\ext\yaml;
/**
* Class YamlConfig: une configuration chargée depuis un fichier yaml
*/
class YamlConfig extends ArrayConfig {
function __construct(string $input) {
parent::__construct(yaml::load($input));
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace nulib\php\types;
use nulib\cl;
class varray {
static function ensure(&$array): void {
if (!is_array($array)) $array = cl::with($array);
}
static function ensuren(&$array): void {
if ($array !== null) varray::ensure($array);
}
static function with($value): array {
self::ensure($value);
return $value;
}
static function withn($value): ?array {
self::ensuren($value);
return $value;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace nulib\php\types;
class vbool {
/** liste de valeurs chaines à considérer comme 'OUI' */
public const YES_VALUES = [
"true", "vrai", "yes", "oui",
"t", "v", "y", "o", "1",
];
/** liste de valeurs chaines à considérer comme 'NON' */
public const NO_VALUES = [
"false", "faux", "non", "no",
"f", "n", "0",
];
/** Vérifier si $value est 'OUI' */
static final function is_yes(?string $value): bool {
if ($value === null) return false;
$value = strtolower(trim($value));
if (in_array($value, self::YES_VALUES, true)) return true;
// n'importe quelle valeur numérique
if (is_numeric($value)) return $value != 0;
return false;
}
/** Vérifier si $value est 'NON' */
static final function is_no(?string $value): bool {
if ($value === null) return true;
$value = strtolower(trim($value));
return in_array($value, self::NO_VALUES, true);
}
static function ensure(&$bool): void {
if (is_string($bool)) {
if (self::is_yes($bool)) $bool = true;
elseif (self::is_no($bool)) $bool = false;
}
if (!is_bool($bool)) $bool = boolval($bool);
}
static function ensuren(&$bool): void {
if ($bool !== null) self::ensure($bool);
}
static function with($value): bool {
self::ensure($value);
return $value;
}
static function withn($value): ?bool {
self::ensuren($value);
return $value;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace nulib\php\types;
class vcontent {
static function ensure(&$content): void {
if ($content === null || $content === false) $content = [];
elseif (!is_string($content) && !is_array($content)) $content = strval($content);
}
static function ensuren(&$content): void {
if ($content !== null) self::ensure($content);
}
/** @return string|array */
static function with($value) {
self::ensure($value);
return $value;
}
/** @return string|array|null */
static function withn($value) {
self::ensuren($value);
return $value;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace nulib\php\types;
class vfloat {
static function ensure(&$float): void {
if (!is_float($float)) $float = floatval($float);
}
static function ensuren(&$float): void {
if ($float !== null) self::ensure($float);
}
static function with($value): float {
self::ensure($value);
return $value;
}
static function withn($value): ?float {
self::ensuren($value);
return $value;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace nulib\php\types;
use nulib\php\func;
class vfunc {
static function ensure(&$func): void {
$func = func::ensure($func);
}
static function ensuren(&$func): void {
if ($func !== null) $func = func::ensure($func);
}
static function with($value): func {
return func::with($value);
}
static function withn($value): ?func {
return func::withn($value);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace nulib\php\types;
class vint {
static function ensure(&$int): void {
if (!is_int($int)) $int = intval($int);
}
static function ensuren(&$int): void {
if ($int !== null) self::ensure($int);
}
static function with($value): int {
self::ensure($value);
return $value;
}
static function withn($value): ?int {
self::ensuren($value);
return $value;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace nulib\php\types;
class vkey {
static function ensure(&$key): void {
if ($key === null) $key = "";
elseif ($key === false) $key = 0;
elseif (!is_string($key) && !is_int($key)) $key = strval($key);
}
static function ensuren(&$key): void {
if ($key !== null) self::ensure($key);
}
/** @return string|int */
static function with($value) {
self::ensure($value);
return $value;
}
/** @return string|int|null */
static function withn($value) {
self::ensuren($value);
return $value;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace nulib\php\types;
class vpkey {
static function ensure(&$pkey): void {
if ($pkey === null) $pkey = "";
elseif ($pkey === false) $pkey = 0;
elseif (!is_string($pkey) && !is_int($pkey) && !is_array($pkey)) $pkey = strval($pkey);
if (is_array($pkey)) {
foreach ($pkey as &$key) {
vkey::ensure($key);
};
unset($key);
}
}
static function ensuren(&$pkey): void {
if ($pkey !== null) self::ensure($pkey);
}
/** @return string|int|array */
static function with($value) {
self::ensure($value);
return $value;
}
/** @return string|int|array|null */
static function withn($value) {
self::ensuren($value);
return $value;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace nulib\php\types;
use nulib\str;
class vrawstring {
/** @var bool faut-il trimmer la valeur */
const TRIM = false;
/** @var bool faut-il normaliser les caractères fin de ligne */
const NORM_NL = false;
static function ensure(&$string): void {
if (!is_string($string)) $string = strval($string);
if (static::TRIM) $string = trim($string);
if (static::NORM_NL) $string = str::norm_nl($string);
}
static function ensuren(&$string): void {
if ($string !== null) self::ensure($string);
}
static function with($value): string {
self::ensure($value);
return $value;
}
static function withn($value): ?string {
self::ensuren($value);
return $value;
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace nulib\php\types;
class vstring extends vrawstring {
const TRIM = true;
}

View File

@ -0,0 +1,7 @@
<?php
namespace nulib\php\types;
class vtext extends vrawstring {
const TRIM = true;
const NORM_NL = true;
}

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

@ -0,0 +1,132 @@
<?php
namespace nulib\app {
use nulib\tests\TestCase;
use nulib\app\impl\config;
use nulib\app\impl\myapp;
use nulib\app\impl\MyApplication1;
use nulib\app\impl\MyApplication2;
class appTest extends TestCase {
function testWith() {
$projdir = config::get_projdir();
$cwd = getcwd();
myapp::reset();
$app1 = myapp::with(MyApplication1::class);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
"bindir" => "$projdir/vendor/bin",
"autoload" => "$projdir/vendor/autoload.php",
],
"appcode" => "nur-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() {
}
}
}

View File

@ -0,0 +1,159 @@
<?php
namespace nulib\app\cli;
use nulib\app\args\Aodef;
use nur\t\TestCase;
class AodefTest extends TestCase {
protected static function assertArg(
Aodef $aodef,
array $options,
bool $haveShortOptions, bool $haveLongOptions, bool $isCommand,
bool $haveArgs, ?int $minArgs, ?int $maxArgs, ?string $argsdesc
) {
$aodef->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, "");
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace nulib\app\cli;
use nulib\app\args\Aogroup;
use nulib\app\args\Aolist;
use nulib\app\args\Aosection;
use nur\t\TestCase;
class AolistTest extends TestCase {
function testGroup() {
$aogroup = new Aogroup([
"group",
["--gopt1"],
["--gopt2"],
], true);
echo "$aogroup\n";
self::assertTrue(true);
}
function testSection() {
$aosection = new Aosection([
["--sopt"],
["group",
["--sgopt1"],
["--sgopt2"],
],
], true);
echo "$aosection\n";
self::assertTrue(true);
}
function testList() {
$aolist = new class([
"param" => "value",
["--opt"],
["group",
["--gopt1"],
["--gopt2"],
],
"sections" => [
[
["--s0opt"],
["group",
["--s0gopt1"],
["--s0gopt2"],
],
],
"ns" => [
["--nsopt"],
["group",
["--nsgopt1"],
["--nsgopt2"],
],
],
],
]) extends Aolist {};
echo "$aolist\n";
self::assertTrue(true);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace nulib\app\cli;
use nulib\app\args\SimpleAolist;
use nur\t\TestCase;
class SimpleAolistTest extends TestCase {
function testOverride() {
$aolist = new SimpleAolist([
["-o", "--longx"],
"merge" => [
["-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);
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace nulib\app\cli;
use nulib\app\args\SimpleArgsParser;
use nur\t\TestCase;
class SimpleArgsParserTest extends TestCase {
const NORMALIZE_ARGS = [
["-a"],
["--longb"],
["-c", "--longc"],
["-x", "--x1"],
["-x", "--x2"],
["-n", "--none"],
["-m:", "--mandatory"],
["-o::", "--optional"],
["--mo02:", "args" => [["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);
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace nulib\app\config {
use PHPUnit\Framework\TestCase;
use nulib\app\config\impl\result;
use nulib\app\config\impl\config1;
use nulib\app\config\impl\config2;
class ConfigManagerTest extends TestCase {
function testConfigurators() {
$config = new ConfigManager();
result::reset();
$config->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";
}
}
}