gestion des configurations

This commit is contained in:
Jephté Clain 2025-10-02 12:31:16 +04:00
parent 373910b170
commit 85f18bd292
9 changed files with 544 additions and 2 deletions

View File

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

View File

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

View File

@ -0,0 +1,50 @@
<?php
namespace nulib\app\config;
use nulib\cl;
class ArrayConfig implements IConfig {
protected function APP(): array {
return static::APP;
} const APP = [];
protected function DBS(): array {
return static::DBS;
} const DBS = [];
protected function MSGS(): array {
return static::MSGS;
} const MSGS = [];
protected function MAILS(): array {
return static::MAILS;
} const MAILS = [];
function __construct(?array $config) {
foreach (self::CONFIG_KEYS as $key) {
switch ($key) {
case "app": $default = $this->APP(); break;
case "dbs": $default = $this->DBS(); break;
case "msgs": $default = $this->MSGS(); break;
case "mails": $default = $this->MAILS(); break;
default: $default = [];
}
$config[$key] ??= $default;
}
$this->config = $config;
}
protected array $config;
function has(string $pkey, string $profile): bool {
return cl::phas($this->config, $pkey);
}
function get(string $pkey, string $profile) {
return cl::pget($this->config, $pkey);
}
function set(string $pkey, $value, string $profile): void {
cl::pset($this->config, $pkey, $value);
}
}

View File

@ -1,5 +1,150 @@
<?php
namespace nulib\app\config;
use nulib\A;
use nulib\app\app;
use nulib\cl;
use nulib\php\func;
use nulib\ValueException;
use ReflectionClass;
class ConfigManager {
protected array $configurators = [];
/** ajouter une classe ou un objet à la liste des configurateurs */
function addConfigurator($configurators): void {
A::merge($this->configurators, cl::with($configurators));
}
protected array $configured = [];
/**
* configurer les objets et les classes qui ne l'ont pas encore été. la liste
* des objets et des classes à configurer est fournie en appelant la méthode
* {@link addConfigurator()}
*
* par défaut, la configuration se fait en appelant toutes les méthodes
* publiques des objets et toutes les méthodes statiques des classes qui
* commencent par 'configure', e.g 'configureThis()' ou 'configure_db()',
* si elles n'ont pas déjà été appelées
*
* Il est possible de modifier la liste des méthodes appelées avec le tableau
* $params, qui doit être conforme au schema de {@link func::CALL_ALL_SCHEMA}
*/
function configure(?array $params=null): void {
$params["prefix"] ??= "configure";
foreach ($this->configurators as $key => $configurator) {
$configured =& $this->configured[$key];
/** @var func[] $methods */
$methods = func::get_all($configurator, $params);
foreach ($methods as $method) {
$name = $method->getName() ?? "(no name)";
$done = $configured[$name] ?? false;
if (!$done) {
$method->invoke();
$configured[$name] = true;
}
}
}
}
#############################################################################
protected $cache = [];
protected function resetCache(): void {
$this->cache = [];
}
protected function cacheHas(string $pkey, string $profile) {
return array_key_exists("$profile.$pkey", $this->cache);
}
protected function cacheGet(string $pkey, string $profile) {
return cl::get($this->cache, "$profile.$pkey");
}
protected function cacheSet(string $pkey, $value, string $profile): void {
$this->cache["$profile.$pkey"] = $value;
}
protected array $profileConfigs = [];
/**
* Ajouter une configuration valide pour le(s) profil(s) spécifié(s)
*
* $config est un objet ou une classe qui définit une ou plusieurs des
* constantes APP, DBS, MSGS, MAILS
*
* si $inProfiles===null, la configuration est valide dans tous les profils
*/
function addConfig($config, ?array $inProfiles=null): void {
if (is_string($config)) {
$c = new ReflectionClass($config);
if ($c->implementsInterface(IConfig::class)) {
$config = $c->newInstance();
} else {
$config = [];
foreach (IConfig::CONFIG_KEYS as $key) {
$config[$key] = cl::with($c->getConstant(strtoupper($key)));
}
$config = new ArrayConfig($config);
}
} elseif (is_array($config)) {
$config = new ArrayConfig($config);
} elseif (!($config instanceof IConfig)) {
throw ValueException::invalid_type($config, "array|IConfig");
}
$inProfiles ??= [IConfig::PROFILE_ALL];
foreach ($inProfiles as $profile) {
$this->profileConfigs[$profile][] = $config;
}
$this->resetCache();
}
function _getValue(string $pkey, $default, string $inProfile) {
$profiles = [$inProfile];
if ($inProfile !== IConfig::PROFILE_ALL) $profiles[] = IConfig::PROFILE_ALL;
$value = $default;
foreach ($profiles as $profile) {
/** @var IConfig[] $configs */
$configs = $this->profileConfigs[$profile] ?? [];
foreach (array_reverse($configs) as $config) {
if ($config->has($pkey, $profile)) {
$value = $config->get($pkey, $profile);
break;
}
}
}
return $value;
}
/**
* obtenir la valeur au chemin de clé $pkey dans le profil spécifié
*
* le $inProfile===null, prendre le profil par défaut.
*/
function getValue(string $pkey, $default=null, ?string $inProfile=null) {
$inProfile ??= app::get_profile();
if ($this->cacheHas($pkey, $inProfile)) {
return $this->cacheGet($pkey, $inProfile);
}
$value = $this->_getValue($pkey, $default, $inProfile);
$this->cacheSet($pkey, $default, $inProfile);
return $value;
}
function setValue(string $pkey, $value, ?string $inProfile=null): void {
$inProfile ??= app::get_profile();
/** @var IConfig[] $configs */
$configs =& $this->profileConfigs[$inProfile];
if ($configs === null) $key = 0;
else $key = array_key_last($configs);
$configs[$key] ??= new ArrayConfig([]);
$configs[$key]->set($pkey, $value, $inProfile);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace nulib\app\config;
use nulib\cl;
use nulib\ext\json;
use nulib\file;
use nulib\str;
/**
* Class EnvConfig: configuration extraite depuis les variables d'environnement
*
* les variables doivent être de la forme {PREFIX}_{PROFILE}_{PKEY} :
* - PREFIX vaut CONFIG, FILE_CONFIG, JSON_CONFIG ou JSON_FILE_CONFIG
* - PROFILE est le profil dans lequel la configuration est valide, ou ALL si
* la valeur doit être valide dans tous les profils
* - PKEY est le chemin de clé dans lequel les caractères '.' sont remplacés
* par '__'
*
* par exemple, la valeur dbs.my_auth.type du profil par défaut est pris dans
* la variable 'CONFIG_ALL_dbs__my_auth__type'. pour le profil prod c'est la
* variable 'CONFIG_prod_dbs__my_auth__type'
*
* pour représenter le tableau suivant:
* [ "type" => "mysql", "name" => "mysql:host=authdb;dbname=auth;charset=utf8",
* "user" => "auth_int", "pass" => "auth" ]
* situé au chemin de clé dbs.auth dans le profil prod, on peut par exemple
* définir les variables suivantes:
* CONFIG_prod_dbs__auth__type="mysql"
* CONFIG_prod_dbs__auth__name="mysql:host=authdb;dbname=auth;charset=utf8"
* CONFIG_prod_dbs__auth__user="auth_int"
* CONFIG_prod_dbs__auth__pass="auth"
* ou alternativement:
* JSON_CONFIG_prod_dbs__auth='{"type":"mysql","name":"mysql:host=authdb;dbname=auth;charset=utf8","user":"auth_int","pass":"auth"}'
*
* Les préfixes supportés sont, dans l'ordre de précédence:
* - JSON_FILE_CONFIG -- une valeur au format JSON inscrite dans un fichier
* - JSON_CONFIG -- une valeur au format JSON
* - FILE_CONFIG -- une valeur inscrite dans un fichier
* - CONFIG -- une valeur scalaire
*/
class EnvConfig implements IConfig{
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);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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";
}
}
}