From 85f18bd292e48207e402acdfb29d016a94400d21 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 2 Oct 2025 12:31:16 +0400 Subject: [PATCH] gestion des configurations --- src/app/app.php | 47 ++++++++ src/app/config.php | 18 ++- src/app/config/ArrayConfig.php | 50 +++++++++ src/app/config/ConfigManager.php | 145 +++++++++++++++++++++++++ src/app/config/EnvConfig.php | 112 +++++++++++++++++++ src/app/config/IConfig.php | 24 ++++ src/app/config/JsonConfig.php | 13 +++ src/app/config/YamlConfig.php | 13 +++ tests/app/config/ConfigManagerTest.php | 124 +++++++++++++++++++++ 9 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 src/app/config/ArrayConfig.php create mode 100644 src/app/config/EnvConfig.php create mode 100644 src/app/config/IConfig.php create mode 100644 src/app/config/JsonConfig.php create mode 100644 src/app/config/YamlConfig.php create mode 100644 tests/app/config/ConfigManagerTest.php diff --git a/src/app/app.php b/src/app/app.php index 037064d..8022a3a 100644 --- a/src/app/app.php +++ b/src/app/app.php @@ -110,11 +110,38 @@ class app { 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): bool { + return self::get()->isFact($fact); + } + + 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 */ @@ -295,6 +322,26 @@ class app { $this->profileManager->setProfile($profile, $productionMode); } + protected ?array $facts; + + function isFact(string $fact): bool { + return $this->facts[$fact] ?? false; + } + + function setFact(string $fact, bool $value=true): void { + $this->facts[$fact] = $value; + } + + protected bool $debug = false; + + function isDebug(): bool { + return $this->debug; + } + + function setDebug(bool $debug=true): void { + $this->debug = $debug; + } + /** * @param ?string|false $profile * diff --git a/src/app/config.php b/src/app/config.php index e0f21d1..08699f9 100644 --- a/src/app/config.php +++ b/src/app/config.php @@ -2,7 +2,6 @@ namespace nulib\app; use nulib\app\config\ConfigManager; -use nulib\app\config\ProfileManager; use nulib\cl; /** @@ -12,12 +11,27 @@ class config { protected static ConfigManager $config; static function init_configurator($configurators): void { - self::$config->addConfigurators(cl::with($configurators)); + 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 { diff --git a/src/app/config/ArrayConfig.php b/src/app/config/ArrayConfig.php new file mode 100644 index 0000000..6a02c8e --- /dev/null +++ b/src/app/config/ArrayConfig.php @@ -0,0 +1,50 @@ +APP(); break; + case "dbs": $default = $this->DBS(); break; + case "msgs": $default = $this->MSGS(); break; + case "mails": $default = $this->MAILS(); break; + default: $default = []; + } + $config[$key] ??= $default; + } + $this->config = $config; + } + + protected array $config; + + function has(string $pkey, string $profile): bool { + return cl::phas($this->config, $pkey); + } + + function get(string $pkey, string $profile) { + return cl::pget($this->config, $pkey); + } + + function set(string $pkey, $value, string $profile): void { + cl::pset($this->config, $pkey, $value); + } +} diff --git a/src/app/config/ConfigManager.php b/src/app/config/ConfigManager.php index cee9fcb..ef10881 100644 --- a/src/app/config/ConfigManager.php +++ b/src/app/config/ConfigManager.php @@ -1,5 +1,150 @@ configurators, cl::with($configurators)); + } + + protected array $configured = []; + + /** + * configurer les objets et les classes qui ne l'ont pas encore été. la liste + * des objets et des classes à configurer est fournie en appelant la méthode + * {@link addConfigurator()} + * + * par défaut, la configuration se fait en appelant toutes les méthodes + * publiques des objets et toutes les méthodes statiques des classes qui + * commencent par 'configure', e.g 'configureThis()' ou 'configure_db()', + * si elles n'ont pas déjà été appelées + * + * Il est possible de modifier la liste des méthodes appelées avec le tableau + * $params, qui doit être conforme au schema de {@link func::CALL_ALL_SCHEMA} + */ + function configure(?array $params=null): void { + $params["prefix"] ??= "configure"; + foreach ($this->configurators as $key => $configurator) { + $configured =& $this->configured[$key]; + /** @var func[] $methods */ + $methods = func::get_all($configurator, $params); + foreach ($methods as $method) { + $name = $method->getName() ?? "(no name)"; + $done = $configured[$name] ?? false; + if (!$done) { + $method->invoke(); + $configured[$name] = true; + } + } + } + } + + ############################################################################# + + protected $cache = []; + + protected function resetCache(): void { + $this->cache = []; + } + + protected function cacheHas(string $pkey, string $profile) { + return array_key_exists("$profile.$pkey", $this->cache); + } + + protected function cacheGet(string $pkey, string $profile) { + return cl::get($this->cache, "$profile.$pkey"); + } + + protected function cacheSet(string $pkey, $value, string $profile): void { + $this->cache["$profile.$pkey"] = $value; + } + + protected array $profileConfigs = []; + + /** + * Ajouter une configuration valide pour le(s) profil(s) spécifié(s) + * + * $config est un objet ou une classe qui définit une ou plusieurs des + * constantes APP, DBS, MSGS, MAILS + * + * si $inProfiles===null, la configuration est valide dans tous les profils + */ + function addConfig($config, ?array $inProfiles=null): void { + if (is_string($config)) { + $c = new ReflectionClass($config); + if ($c->implementsInterface(IConfig::class)) { + $config = $c->newInstance(); + } else { + $config = []; + foreach (IConfig::CONFIG_KEYS as $key) { + $config[$key] = cl::with($c->getConstant(strtoupper($key))); + } + $config = new ArrayConfig($config); + } + } elseif (is_array($config)) { + $config = new ArrayConfig($config); + } elseif (!($config instanceof IConfig)) { + throw ValueException::invalid_type($config, "array|IConfig"); + } + + $inProfiles ??= [IConfig::PROFILE_ALL]; + foreach ($inProfiles as $profile) { + $this->profileConfigs[$profile][] = $config; + } + + $this->resetCache(); + } + + function _getValue(string $pkey, $default, string $inProfile) { + $profiles = [$inProfile]; + if ($inProfile !== IConfig::PROFILE_ALL) $profiles[] = IConfig::PROFILE_ALL; + $value = $default; + foreach ($profiles as $profile) { + /** @var IConfig[] $configs */ + $configs = $this->profileConfigs[$profile] ?? []; + foreach (array_reverse($configs) as $config) { + if ($config->has($pkey, $profile)) { + $value = $config->get($pkey, $profile); + break; + } + } + } + return $value; + } + + /** + * obtenir la valeur au chemin de clé $pkey dans le profil spécifié + * + * le $inProfile===null, prendre le profil par défaut. + */ + function getValue(string $pkey, $default=null, ?string $inProfile=null) { + $inProfile ??= app::get_profile(); + + if ($this->cacheHas($pkey, $inProfile)) { + return $this->cacheGet($pkey, $inProfile); + } + + $value = $this->_getValue($pkey, $default, $inProfile); + $this->cacheSet($pkey, $default, $inProfile); + return $value; + } + + function setValue(string $pkey, $value, ?string $inProfile=null): void { + $inProfile ??= app::get_profile(); + /** @var IConfig[] $configs */ + $configs =& $this->profileConfigs[$inProfile]; + if ($configs === null) $key = 0; + else $key = array_key_last($configs); + $configs[$key] ??= new ArrayConfig([]); + $configs[$key]->set($pkey, $value, $inProfile); + } } diff --git a/src/app/config/EnvConfig.php b/src/app/config/EnvConfig.php new file mode 100644 index 0000000..0d93173 --- /dev/null +++ b/src/app/config/EnvConfig.php @@ -0,0 +1,112 @@ + "mysql", "name" => "mysql:host=authdb;dbname=auth;charset=utf8", + * "user" => "auth_int", "pass" => "auth" ] + * situé au chemin de clé dbs.auth dans le profil prod, on peut par exemple + * définir les variables suivantes: + * CONFIG_prod_dbs__auth__type="mysql" + * CONFIG_prod_dbs__auth__name="mysql:host=authdb;dbname=auth;charset=utf8" + * CONFIG_prod_dbs__auth__user="auth_int" + * CONFIG_prod_dbs__auth__pass="auth" + * ou alternativement: + * JSON_CONFIG_prod_dbs__auth='{"type":"mysql","name":"mysql:host=authdb;dbname=auth;charset=utf8","user":"auth_int","pass":"auth"}' + * + * Les préfixes supportés sont, dans l'ordre de précédence: + * - JSON_FILE_CONFIG -- une valeur au format JSON inscrite dans un fichier + * - JSON_CONFIG -- une valeur au format JSON + * - FILE_CONFIG -- une valeur inscrite dans un fichier + * - CONFIG -- une valeur scalaire + */ +class EnvConfig implements IConfig{ + function __construct() { + $this->loadEnvConfig(); + } + + protected array $profileConfigs; + + /** 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 { + $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 { + $config = $this->profileConfigs[$profile] ?? null; + return cl::phas($config, $pkey); + } + + function get(string $pkey, string $profile) { + $config = $this->profileConfigs[$profile] ?? null; + return cl::pget($config, $pkey); + } + + function set(string $pkey, $value, string $profile): void { + $config =& $this->profileConfigs[$profile]; + cl::pset($config, $pkey, $value); + } +} diff --git a/src/app/config/IConfig.php b/src/app/config/IConfig.php new file mode 100644 index 0000000..dcba89f --- /dev/null +++ b/src/app/config/IConfig.php @@ -0,0 +1,24 @@ +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"; + } + } +}