migration d'outils depuis nur-ture
This commit is contained in:
parent
6a41b72e95
commit
efb7901498
1
.idea/nulib-base.iml
generated
1
.idea/nulib-base.iml
generated
@ -4,6 +4,7 @@
|
|||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/php/src" isTestSource="false" packagePrefix="nulib\" />
|
<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/tests" isTestSource="true" packagePrefix="nulib\" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/php/cli" isTestSource="false" packagePrefix="cli\" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/php/vendor" />
|
<excludeFolder url="file://$MODULE_DIR$/php/vendor" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . "/../php/vendor/autoload.php";
|
require __DIR__ . "/../php/vendor/autoload.php";
|
||||||
|
|
||||||
use nulib\tools\pman\ComposerFile;
|
use cli\pman\ComposerFile;
|
||||||
use nulib\tools\pman\ComposerPmanFile;
|
|
||||||
use nulib\ValueException;
|
|
||||||
|
|
||||||
$composer = new ComposerFile();
|
$composer = new ComposerFile();
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . "/../php/vendor/autoload.php";
|
require __DIR__ . "/../php/vendor/autoload.php";
|
||||||
|
|
||||||
use nulib\tools\pman\ComposerFile;
|
use cli\pman\ComposerFile;
|
||||||
use nulib\tools\pman\ComposerPmanFile;
|
use cli\pman\ComposerPmanFile;
|
||||||
use nulib\ValueException;
|
use nulib\ValueException;
|
||||||
|
|
||||||
$composer = new ComposerFile();
|
$composer = new ComposerFile();
|
||||||
|
1
bin/.dumpser.php
Symbolic link
1
bin/.dumpser.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../php/bin/dumpser.php
|
1
bin/.json2yml.php
Symbolic link
1
bin/.json2yml.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../php/bin/json2yml.php
|
1
bin/.mysql.capacitor.php
Symbolic link
1
bin/.mysql.capacitor.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../php/bin/mysql.capacitor.php
|
1
bin/.pgsql.capacitor.php
Symbolic link
1
bin/.pgsql.capacitor.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../php/bin/pgsql.capacitor.php
|
1
bin/.sqlite.capacitor.php
Symbolic link
1
bin/.sqlite.capacitor.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../php/bin/sqlite.capacitor.php
|
1
bin/.yml2json.php
Symbolic link
1
bin/.yml2json.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../php/bin/yml2json.php
|
1
bin/dumpser.php
Symbolic link
1
bin/dumpser.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
runphp
|
1
bin/json2yml.php
Symbolic link
1
bin/json2yml.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
runphp
|
1
bin/mysql.capacitor.php
Symbolic link
1
bin/mysql.capacitor.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
runphp
|
1
bin/pgsql.capacitor.php
Symbolic link
1
bin/pgsql.capacitor.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
runphp
|
1
bin/sqlite.capacitor.php
Symbolic link
1
bin/sqlite.capacitor.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
runphp
|
1
bin/yml2json.php
Symbolic link
1
bin/yml2json.php
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
runphp
|
@ -38,7 +38,8 @@
|
|||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"nulib\\": "php/src"
|
"nulib\\": "php/src",
|
||||||
|
"cli\\": "php/cli"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
@ -46,6 +47,14 @@
|
|||||||
"nulib\\": "php/tests"
|
"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": {
|
"config": {
|
||||||
"vendor-dir": "php/vendor"
|
"vendor-dir": "php/vendor"
|
||||||
},
|
},
|
||||||
|
51
composer.lock
generated
51
composer.lock
generated
@ -420,16 +420,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpmailer/phpmailer",
|
"name": "phpmailer/phpmailer",
|
||||||
"version": "v6.10.0",
|
"version": "v6.11.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
|
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284",
|
||||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -450,6 +450,7 @@
|
|||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
"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-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
"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",
|
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
"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": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -497,7 +498,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-04-24T15:19:31+00:00"
|
"time": "2025-09-30T11:54:53+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/cache",
|
"name": "psr/cache",
|
||||||
@ -2147,16 +2148,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/phpunit",
|
"name": "phpunit/phpunit",
|
||||||
"version": "9.6.25",
|
"version": "9.6.29",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||||
"reference": "049c011e01be805202d8eebedef49f769a8ec7b7"
|
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/049c011e01be805202d8eebedef49f769a8ec7b7",
|
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
|
||||||
"reference": "049c011e01be805202d8eebedef49f769a8ec7b7",
|
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -2181,7 +2182,7 @@
|
|||||||
"sebastian/comparator": "^4.0.9",
|
"sebastian/comparator": "^4.0.9",
|
||||||
"sebastian/diff": "^4.0.6",
|
"sebastian/diff": "^4.0.6",
|
||||||
"sebastian/environment": "^5.1.5",
|
"sebastian/environment": "^5.1.5",
|
||||||
"sebastian/exporter": "^4.0.6",
|
"sebastian/exporter": "^4.0.8",
|
||||||
"sebastian/global-state": "^5.0.8",
|
"sebastian/global-state": "^5.0.8",
|
||||||
"sebastian/object-enumerator": "^4.0.4",
|
"sebastian/object-enumerator": "^4.0.4",
|
||||||
"sebastian/resource-operations": "^3.0.4",
|
"sebastian/resource-operations": "^3.0.4",
|
||||||
@ -2230,7 +2231,7 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
"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": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -2254,7 +2255,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-08-20T14:38:31+00:00"
|
"time": "2025-09-24T06:29:11+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/cli-parser",
|
"name": "sebastian/cli-parser",
|
||||||
@ -2697,16 +2698,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/exporter",
|
"name": "sebastian/exporter",
|
||||||
"version": "4.0.6",
|
"version": "4.0.8",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/exporter.git",
|
"url": "https://github.com/sebastianbergmann/exporter.git",
|
||||||
"reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
|
"reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
|
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c",
|
||||||
"reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
|
"reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -2762,15 +2763,27 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/exporter/issues",
|
"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": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://github.com/sebastianbergmann",
|
"url": "https://github.com/sebastianbergmann",
|
||||||
"type": "github"
|
"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",
|
"name": "sebastian/global-state",
|
||||||
|
7
php/bin/dumpser.php
Executable file
7
php/bin/dumpser.php
Executable 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
7
php/bin/json2yml.php
Executable 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
7
php/bin/mysql.capacitor.php
Executable 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
7
php/bin/pgsql.capacitor.php
Executable 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
7
php/bin/sqlite.capacitor.php
Executable 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
7
php/bin/yml2json.php
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/php
|
||||||
|
<?php
|
||||||
|
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use cli\Yaml2jsonApp;
|
||||||
|
|
||||||
|
Yaml2jsonApp::run();
|
111
php/cli/AbstractCapacitorApp.php
Normal file
111
php/cli/AbstractCapacitorApp.php
Normal 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
122
php/cli/BgLauncherApp.php
Normal 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
31
php/cli/DumpserApp.php
Normal 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
21
php/cli/Json2yamlApp.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
45
php/cli/MysqlCapacitorApp.php
Normal file
45
php/cli/MysqlCapacitorApp.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
45
php/cli/PgsqlCapacitorApp.php
Normal file
45
php/cli/PgsqlCapacitorApp.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
43
php/cli/SqliteCapacitorApp.php
Normal file
43
php/cli/SqliteCapacitorApp.php
Normal 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
53
php/cli/SteamTrainApp.php
Normal 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
21
php/cli/Yaml2jsonApp.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace nulib\tools\pman;
|
namespace cli\pman;
|
||||||
|
|
||||||
use nulib\cl;
|
use nulib\cl;
|
||||||
use nulib\ext\json;
|
use nulib\ext\json;
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace nulib\tools\pman;
|
namespace cli\pman;
|
||||||
|
|
||||||
use nulib\A;
|
use nulib\A;
|
||||||
use nulib\ext\yaml;
|
use nulib\ext\yaml;
|
614
php/src/app/app.php
Normal file
614
php/src/app/app.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
109
php/src/app/args/AbstractArgsParser.php
Normal file
109
php/src/app/args/AbstractArgsParser.php
Normal 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
623
php/src/app/args/Aodef.php
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
38
php/src/app/args/Aogroup.php
Normal file
38
php/src/app/args/Aogroup.php
Normal 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
271
php/src/app/args/Aolist.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
47
php/src/app/args/Aosection.php
Normal file
47
php/src/app/args/Aosection.php
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
20
php/src/app/args/ArgsException.php
Normal file
20
php/src/app/args/ArgsException.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
189
php/src/app/args/SimpleAolist.php
Normal file
189
php/src/app/args/SimpleAolist.php
Normal 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))),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
250
php/src/app/args/SimpleArgsParser.php
Normal file
250
php/src/app/args/SimpleArgsParser.php
Normal 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
21
php/src/app/args/TODO.md
Normal 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
|
383
php/src/app/cli/Application.php
Normal file
383
php/src/app/cli/Application.php
Normal 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
41
php/src/app/config.php
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
50
php/src/app/config/ArrayConfig.php
Normal file
50
php/src/app/config/ArrayConfig.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
150
php/src/app/config/ConfigManager.php
Normal file
150
php/src/app/config/ConfigManager.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
112
php/src/app/config/EnvConfig.php
Normal file
112
php/src/app/config/EnvConfig.php
Normal 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} où:
|
||||||
|
* - 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);
|
||||||
|
}
|
||||||
|
}
|
24
php/src/app/config/IConfig.php
Normal file
24
php/src/app/config/IConfig.php
Normal 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;
|
||||||
|
}
|
13
php/src/app/config/JsonConfig.php
Normal file
13
php/src/app/config/JsonConfig.php
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
142
php/src/app/config/ProfileManager.php
Normal file
142
php/src/app/config/ProfileManager.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
13
php/src/app/config/YamlConfig.php
Normal file
13
php/src/app/config/YamlConfig.php
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
24
php/src/php/types/varray.php
Normal file
24
php/src/php/types/varray.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
54
php/src/php/types/vbool.php
Normal file
54
php/src/php/types/vbool.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
25
php/src/php/types/vcontent.php
Normal file
25
php/src/php/types/vcontent.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
22
php/src/php/types/vfloat.php
Normal file
22
php/src/php/types/vfloat.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
22
php/src/php/types/vfunc.php
Normal file
22
php/src/php/types/vfunc.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
22
php/src/php/types/vint.php
Normal file
22
php/src/php/types/vint.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
26
php/src/php/types/vkey.php
Normal file
26
php/src/php/types/vkey.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
32
php/src/php/types/vpkey.php
Normal file
32
php/src/php/types/vpkey.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
31
php/src/php/types/vrawstring.php
Normal file
31
php/src/php/types/vrawstring.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
6
php/src/php/types/vstring.php
Normal file
6
php/src/php/types/vstring.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\php\types;
|
||||||
|
|
||||||
|
class vstring extends vrawstring {
|
||||||
|
const TRIM = true;
|
||||||
|
}
|
7
php/src/php/types/vtext.php
Normal file
7
php/src/php/types/vtext.php
Normal 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
132
php/tests/app/appTest.php
Normal 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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
159
php/tests/app/cli/AodefTest.php
Normal file
159
php/tests/app/cli/AodefTest.php
Normal 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, "");
|
||||||
|
}
|
||||||
|
}
|
63
php/tests/app/cli/AolistTest.php
Normal file
63
php/tests/app/cli/AolistTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
59
php/tests/app/cli/SimpleAolistTest.php
Normal file
59
php/tests/app/cli/SimpleAolistTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
175
php/tests/app/cli/SimpleArgsParserTest.php
Normal file
175
php/tests/app/cli/SimpleArgsParserTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
124
php/tests/app/config/ConfigManagerTest.php
Normal file
124
php/tests/app/config/ConfigManagerTest.php
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user