581 lines
19 KiB
PHP
581 lines
19 KiB
PHP
<?php
|
|
namespace nur\config;
|
|
|
|
use nur\A;
|
|
use nur\b\ValueException;
|
|
use nur\func;
|
|
use nur\md;
|
|
use nur\session;
|
|
use ReflectionClass;
|
|
|
|
/**
|
|
* Class ConfigManager: gestion de la configuration de l'application
|
|
*/
|
|
class ConfigManager implements IConfigManager {
|
|
/** @var array */
|
|
private $configurators = [];
|
|
|
|
function addConfigurator($configurators): void {
|
|
$configurators = A::with($configurators);
|
|
A::merge($this->configurators, $configurators);
|
|
}
|
|
|
|
/**
|
|
* options par défaut pour filtrer les méthodes appelée dans le cadre de la
|
|
* configuration. cette valeur peut être redéfinie dans une classe dérivée
|
|
*/
|
|
const CONFIGURE_OPTIONS = ["prefix" => "configure"];
|
|
|
|
/** @var array */
|
|
private $configured = [];
|
|
|
|
function configure(?array $options=null): void {
|
|
A::update_nx($options, static::CONFIGURE_OPTIONS);
|
|
$configured =& $this->configured;
|
|
foreach ($this->configurators as $key => $configurator) {
|
|
A::ensure_array($configured[$key]);
|
|
$done =& $configured[$key];
|
|
$methods = func::get_all($configurator, $options);
|
|
foreach ($methods as $method) {
|
|
if (!in_array($method[1], $done)) {
|
|
$args = null;
|
|
func::fix_args($method, $args);
|
|
func::call($method, ...$args);
|
|
$done[] = $method[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetConfiguration(): void {
|
|
$this->configured = [];
|
|
}
|
|
|
|
#############################################################################
|
|
|
|
/** liste d'aliases de classes */
|
|
const CLASS_ALIASES = [
|
|
"ref" => Ref::class,
|
|
"mref" => MergeRef::class,
|
|
"aref" => AppendRef::class,
|
|
"pref" => PrependRef::class,
|
|
"refs" => RefList::class,
|
|
];
|
|
|
|
/**
|
|
* si $class est un alias, retourner le nom effectif de la classe, sinon
|
|
* retourner la valeur inchangée.
|
|
*
|
|
* la constante CLASS_ALIASES, qui peut être redéfinies dans les classes
|
|
* dérivées, est utilisée pour faire le calcul.
|
|
*/
|
|
protected function resolveClassAlias(string $class): string {
|
|
return A::get(static::CLASS_ALIASES, $class, $class);
|
|
}
|
|
|
|
/** @var string[] liste de classes qui représentent des références */
|
|
const REF_CLASSES = [
|
|
Ref::class,
|
|
MergeRef::class,
|
|
AppendRef::class,
|
|
PrependRef::class,
|
|
RefList::class,
|
|
];
|
|
|
|
protected function isRefClass(string $class): bool {
|
|
$class = $this->resolveClassAlias($class);
|
|
return in_array($class, static::REF_CLASSES);
|
|
}
|
|
|
|
/**
|
|
* @var int indiquer que les tableaux ne sont jamais reconnus comme des
|
|
* objets. il faut donc lister les clés de tous les objets dans OBJECT_PKEYS
|
|
* et OBJECT_PKEY_PREFIXES pour qu'ils soient reconnus
|
|
*/
|
|
const OBJECT_AUTODEF_FALSE = 0;
|
|
|
|
/**
|
|
* @var int indiquer que les tableaux de la forme appropriée sont toujours
|
|
* reconnus comme des objets
|
|
*/
|
|
const OBJECT_AUTODEF_TRUE = 1;
|
|
|
|
/**
|
|
* @var int indiquer que les tableaux ne sont reconnus comme des objets que
|
|
* s'il s'agit de références. pour les autres objets, il faut lister les clés
|
|
* dans OBJECT_PKEYS et OBJECT_PKEY_PREFIXES pour qu'ils soient reconnus
|
|
*/
|
|
const OBJECT_AUTODEF_REF_ONLY = 2;
|
|
|
|
/**
|
|
* stratégie pour reconnaitre comme objets les tableaux de la forme appropriée
|
|
*/
|
|
protected function OBJECT_AUTODEF(): int {
|
|
return static::OBJECT_AUTODEF;
|
|
} const OBJECT_AUTODEF = self::OBJECT_AUTODEF_REF_ONLY;
|
|
|
|
/**
|
|
* liste de chemins de clé à partir desquels il n'est pas nécessaire de
|
|
* tester s'il faut retourner un objet
|
|
*/
|
|
protected function NOT_OBJECT_PKEY_PREFIXES(): array {
|
|
return static::NOT_OBJECT_PKEY_PREFIXES;
|
|
} const NOT_OBJECT_PKEY_PREFIXES = [];
|
|
|
|
function isNotObject(string $pkey): bool {
|
|
$notObjectPrefixes = $this->NOT_OBJECT_PKEY_PREFIXES();
|
|
foreach ($notObjectPrefixes as $prefix) {
|
|
if ($pkey === $prefix) return true;
|
|
$prefixLength = strlen($prefix) + 1;
|
|
if (substr($pkey, 0, $prefixLength) == "${prefix}.") {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* liste de chemins de clé (sous forme d'associations $pkey => $class) pour
|
|
* lesquels on doit retourner un objet. le chemin doit correspondre exactement
|
|
* pour qu'on objet soit retourné.
|
|
*
|
|
* par exemple, pour le chemin "x.y.z", les chemins "x.y" et "x.y.z.t"
|
|
* ne retournent pas d'objet. par contre "x.y.z" retourne un objet
|
|
*
|
|
* si $class === null, alors la valeur au chemin de clé doit être au format
|
|
* [[$class], ...$args] pour pouvoir être reconnue par {@link newObject()}
|
|
*/
|
|
protected function OBJECT_PKEYS(): array {
|
|
return static::OBJECT_PKEYS;
|
|
} const OBJECT_PKEYS = [];
|
|
|
|
/**
|
|
* liste de *préfixes* de chemin de clé (sous forme d'associations
|
|
* $pkey_prefix => $class) pour lesquels on doit retourner un objet. le
|
|
* préfixe n'est considéré que pour le niveau immédiatement suivant.
|
|
*
|
|
* par exemple, pour le préfixe "x.y", les chemins de clé "x.y" et "x.y.z.t"
|
|
* ne retournent pas d'objet. par contre "x.y.z" retourne un objet
|
|
*
|
|
* si $class === null, alors la valeur au chemin de clé doit être au format
|
|
* [[$class], ...$args] pour pouvoir être reconnue par {@link newObject()}
|
|
*/
|
|
protected function OBJECT_PKEY_PREFIXES(): array {
|
|
return static::OBJECT_PKEY_PREFIXES;
|
|
} const OBJECT_PKEY_PREFIXES = [];
|
|
|
|
function isObject($definition, ?string $pkey=null): bool {
|
|
if ($pkey !== null) {
|
|
# tester les exclusions
|
|
if ($this->isNotObject($pkey)) return false;
|
|
# tester l'égalité
|
|
$objectPkeys = $this->OBJECT_PKEYS();
|
|
if (array_key_exists($pkey, $objectPkeys)) return true;
|
|
# tester les préfixes
|
|
$objectPrefixes = $this->OBJECT_PKEY_PREFIXES();
|
|
foreach ($objectPrefixes as $prefix => $class) {
|
|
$prefixLength = strlen($prefix) + 1;
|
|
if (substr($pkey, 0, $prefixLength) == "${prefix}.") {
|
|
if (strpos($pkey, ".", $prefixLength + 1) === false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$objectAutodef = $this->OBJECT_AUTODEF();
|
|
if ($objectAutodef == self::OBJECT_AUTODEF_FALSE) return false;
|
|
# sinon, $definition *doit* être un array de la forme [[$class], $args...]
|
|
# ou [[$class, $method], $args...]
|
|
if (is_array($definition) && array_key_exists(0, $definition)) {
|
|
$class = $definition[0];
|
|
if (is_array($class)
|
|
&& count($class) == 1
|
|
&& array_key_exists(0, $class) && is_string($class[0])) {
|
|
# [$class]
|
|
return $objectAutodef != self::OBJECT_AUTODEF_REF_ONLY
|
|
|| $this->isRefClass($class[0]);
|
|
}
|
|
if (is_array($class)
|
|
&& count($class) == 2
|
|
&& array_key_exists(0, $class) && is_string($class[0])
|
|
&& array_key_exists(1, $class) && is_string($class[1])
|
|
) {
|
|
# [$class, $method]
|
|
return $objectAutodef != self::OBJECT_AUTODEF_REF_ONLY
|
|
|| $this->isRefClass($class[0]);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* cette implémentation ne supporte que les définitions tableau.
|
|
*
|
|
* la syntaxe de définition est un peu plus étendue que celle reconnue par
|
|
* is_object() pour supporter les définitions sélectionnées par un préfixe:
|
|
* - [[$class], $args...] --> instanciation
|
|
* - [$class, $args...] --> instanciation
|
|
* - [[$class, $method], $args...] --> appel de méthode statique
|
|
*
|
|
* XXX ajouter le support d'un marqueur qui indique qu'un tableau N'EST PAS un
|
|
* objet, genre [["NOT-AN-OBJECT"], content...], et cette méthode renvoie le
|
|
* tableau sans le marqueur
|
|
*
|
|
* @throws ConfigException si $definition n'est pas un tableau
|
|
*/
|
|
function newObject($definition, ?string $pkey=null) {
|
|
#XXX supporter les définitions de classes de $OBJECT_PKEYS et $OBJECT_PKEY_PREFIXES
|
|
if ($definition === null) {
|
|
throw new ConfigException("definition must not be null");
|
|
} elseif (!is_array($definition)) {
|
|
throw new ConfigException("definition must be an array");
|
|
}
|
|
[$seq, $assoc] = A::split_assoc($definition);
|
|
$class = A::get($seq, 0);
|
|
if ($class === null) {
|
|
throw new ConfigException("definition's class is missing");
|
|
}
|
|
$args = array_slice($seq, 1);
|
|
if ($assoc !== null) $args[] = $assoc;
|
|
if (is_array($class) && is_callable($class, true)) {
|
|
# méthode [$class, $method]
|
|
return func::call($class, ...$args);
|
|
} else {
|
|
# classe [$class] ou $class
|
|
if (is_array($class)) $class = $class[0];
|
|
$class = $this->resolveClassAlias($class);
|
|
return func::cons($class, ...$args);
|
|
}
|
|
}
|
|
|
|
#############################################################################
|
|
|
|
/** @var string */
|
|
protected $appcode = "NOT-SET";
|
|
|
|
function initAppcode(string $appcode): void {
|
|
$this->appcode = $appcode;
|
|
}
|
|
|
|
function getAppcode(): string {
|
|
return $this->appcode;
|
|
}
|
|
|
|
protected $defaultProfile;
|
|
|
|
/**
|
|
* spécifier le profil à sélectionner par défaut si l'utilisateur n'en n'a pas
|
|
* fourni
|
|
*/
|
|
function setDefaultProfile(?string $defaultProfile): void {
|
|
$this->defaultProfile = $defaultProfile;
|
|
}
|
|
|
|
/**
|
|
* chaque clé de ce tableau est la configuration pour un profil.
|
|
* la clé self::ALL_PROFILE représente tous les profils
|
|
*
|
|
* pour chaque profil, la valeur est une liste d'instances de DynConfig ou de
|
|
* tableaux conformes au schéma CONFIG_SCHEMA
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $profileConfigs;
|
|
|
|
/**
|
|
* s'assurer que le tableau $configs a la structure appropriée à la liste des
|
|
* profils spécifiés
|
|
*/
|
|
private function ensureConfigs(string ...$profiles) {
|
|
$profileConfigs =& $this->profileConfigs;
|
|
if (!A::has($profileConfigs, self::PROFILE_ALL)) {
|
|
# le profil par défaut doit exister
|
|
$profiles[] = self::PROFILE_ALL;
|
|
}
|
|
foreach ($profiles as $profile) {
|
|
A::ensure_array($profileConfigs[$profile]);
|
|
}
|
|
}
|
|
|
|
function getProfiles(): array {
|
|
if ($this->profileConfigs === null) {
|
|
# initialiser si nécessaire
|
|
$profiles = A::with(static::DEFAULT_PROFILES);
|
|
# s'assurer que le tableau $configs contiennent les clés correspondant à
|
|
# tous les profils
|
|
$this->ensureConfigs(...$profiles);
|
|
}
|
|
$profiles = array_keys($this->profileConfigs);
|
|
A::del_value($profiles, self::PROFILE_ALL);
|
|
return $profiles;
|
|
}
|
|
|
|
const PROFILE_SESSION_KEY = "config:current_profile";
|
|
const PROFILE_ENV_KEY = "APP_PROFILE";
|
|
|
|
/** @var string */
|
|
protected $profile;
|
|
|
|
protected function initProfile(): string {
|
|
$profile = getenv(self::PROFILE_ENV_KEY);
|
|
if ($profile === false) $profile = null;
|
|
if ($profile === null) $profile = $this->defaultProfile;
|
|
if ($profile === null) $profile = A::get($this->getProfiles(), 0);
|
|
if ($profile === null) $profile = self::PROD;
|
|
$this->profile = $profile;
|
|
if (session::started()) {
|
|
session::set(self::PROFILE_SESSION_KEY, $profile);
|
|
}
|
|
return $profile;
|
|
}
|
|
|
|
function getProfile(bool $ensureNn=true): ?string {
|
|
$profile = $this->profile;
|
|
$update_session = false;
|
|
if (session::started()) {
|
|
$sprofile = session::get(self::PROFILE_SESSION_KEY);
|
|
if ($sprofile !== $profile) $update_session = true;
|
|
if ($sprofile !== null) $profile = $sprofile;
|
|
}
|
|
if ($profile === null) {
|
|
if (!$ensureNn) return null;
|
|
# initProfile() met à jour la session si nécessaire
|
|
$profile = $this->initProfile();
|
|
}
|
|
if ($update_session) session::set(self::PROFILE_SESSION_KEY, $profile);
|
|
return $profile;
|
|
}
|
|
|
|
function setProfile(string $profile): void {
|
|
$this->profile = $profile;
|
|
if (session::started()) {
|
|
session::set(self::PROFILE_SESSION_KEY, $profile);
|
|
}
|
|
}
|
|
|
|
#############################################################################
|
|
|
|
const CONFIG_SCHEMA = [
|
|
"schema" => [null, [], "définition des schémas"],
|
|
"dbs" => [null, [], "définitions des paramètres de connexion"],
|
|
"msgs" => [null, [], "messages de l'application"],
|
|
"mails" => [null, [], "modèles de mail"],
|
|
"app" => [null, [], "variables applicatives"],
|
|
"user" => [null, [], "variables utilisateur"],
|
|
];
|
|
|
|
private $cache = [];
|
|
|
|
private final function resetCache(): void {
|
|
$this->cache = [];
|
|
}
|
|
|
|
private final function cacheHas(string $pkey, string $profile) {
|
|
$ckey = "$profile.$pkey";
|
|
return array_key_exists($ckey, $this->cache);
|
|
}
|
|
private final function cacheGet(string $pkey, string $profile) {
|
|
$ckey = "$profile.$pkey";
|
|
return A::get($this->cache, $ckey);
|
|
}
|
|
private final function cacheSet(string $pkey, $value, string $profile): void {
|
|
$ckey = "$profile.$pkey";
|
|
$this->cache[$ckey] = $value;
|
|
}
|
|
|
|
function addConfig($config, ?array $inProfiles=null): void {
|
|
if (is_string($config)) {
|
|
$c = new ReflectionClass($config);
|
|
if ($c->isSubclassOf(DynConfig::class)) {
|
|
# si c'est une sous-classe de DynConfig, l'instancier
|
|
$config = $c->newInstance();
|
|
} else {
|
|
# sinon, prendre les constantes directement dans la classe
|
|
$config = [];
|
|
foreach (self::CONFIG_SCHEMA as $key => $ignored) {
|
|
$config[$key] = A::with($c->getConstant(strtoupper($key)));
|
|
}
|
|
}
|
|
} elseif ($config instanceof DynConfig) {
|
|
# l'ajouter tel quel
|
|
} elseif (is_array($config)) {
|
|
md::ensure_schema($config, self::CONFIG_SCHEMA, null, false);
|
|
} else {
|
|
throw new ValueException("config must be a class, an array, or an instance of ".DynConfig::class);
|
|
}
|
|
|
|
if ($this->profileConfigs === null) $this->getProfiles();
|
|
if ($inProfiles !== null) $this->ensureConfigs(...$inProfiles);
|
|
|
|
$profileConfigs =& $this->profileConfigs;
|
|
if (!$inProfiles) $inProfiles = array_keys($profileConfigs);
|
|
if (!$inProfiles) $inProfiles = [self::PROFILE_ALL];
|
|
foreach ($inProfiles as $profile) {
|
|
$profileConfigs[$profile][] = $config;
|
|
}
|
|
|
|
// vider le cache, la nouvelle configure peut changer les choses
|
|
$this->resetCache();
|
|
}
|
|
|
|
private function resolveObjects(array &$array, string $pkey, string $profile): void {
|
|
if ($this->isNotObject($pkey)) return;
|
|
if ($this->isObject($array, $pkey)) {
|
|
$array = $this->newObject($array, $pkey);
|
|
# mettre en cache
|
|
$this->cacheSet($pkey, $array, $profile);
|
|
return;
|
|
}
|
|
foreach ($array as $key => &$value) {
|
|
if (!is_array($value)) continue;
|
|
$value_pkey = $pkey;
|
|
if ($value_pkey != "") $value_pkey .= ".";
|
|
$value_pkey .= $pkey;
|
|
if ($this->cacheHas($value_pkey, $profile)) {
|
|
$value = $this->cacheGet($value_pkey, $profile);
|
|
} else {
|
|
$this->resolveObjects($value, $value_pkey, $profile);
|
|
}
|
|
}; unset($value);
|
|
}
|
|
|
|
private function resolveRef(Ref $ref, string $pkey, string $profile) {
|
|
return $ref->resolve($this, $pkey, $profile);
|
|
}
|
|
private function resolveRefs(array &$array, string $pkey, string $profile): void {
|
|
foreach ($array as $key => &$value) {
|
|
$value_pkey = $pkey;
|
|
if ($value_pkey != "") $value_pkey .= ".";
|
|
$value_pkey .= $pkey;
|
|
if ($value instanceof Ref) {
|
|
$value = $this->resolveRef($value, $value_pkey, $profile);
|
|
} elseif (is_array($value)) {
|
|
$this->resolveRefs($value, $value_pkey, $profile);
|
|
}
|
|
}; unset($value);
|
|
}
|
|
|
|
function _getValue(string $pkey, $default, string $inProfile) {
|
|
## obtenir la valeur brute
|
|
if (strpos($pkey, ".") === false) {
|
|
# pour les clés de premier niveau, il faut merger les valeurs de tous
|
|
# les profils
|
|
if ($inProfile === self::PROFILE_ALL) $profiles = [$inProfile];
|
|
else $profiles = [self::PROFILE_ALL, $inProfile];
|
|
$value = [];
|
|
foreach ($profiles as $profile) {
|
|
$configs = $this->profileConfigs[$profile];
|
|
foreach ($configs as $config) {
|
|
if ($config instanceof DynConfig) {
|
|
# ignorer les configuration dynamiques
|
|
continue;
|
|
}
|
|
A::merge($value, $config[$pkey]);
|
|
}
|
|
}
|
|
$found = true;
|
|
} else {
|
|
# pour les clés à partir du deuxième niveau, prendre la valeur dans la première
|
|
# config qui contient la clé
|
|
if ($inProfile === self::PROFILE_ALL) $profiles = [$inProfile];
|
|
else $profiles = [$inProfile, self::PROFILE_ALL];
|
|
$value = null;
|
|
$found = false;
|
|
foreach ($profiles as $profile) {
|
|
$configs = $this->profileConfigs[$profile];
|
|
$count = count($configs);
|
|
for ($i = $count - 1; $i >= 0; $i--) {
|
|
# parcourir les configurations dans l'ordre inverse
|
|
$config = $configs[$i];
|
|
if ($config instanceof DynConfig) {
|
|
if ($config->has($pkey, $profile)) {
|
|
$found = true;
|
|
$value = $config->get($pkey, $profile);
|
|
break;
|
|
}
|
|
} else {
|
|
if (A::phas_s($config, $pkey)) {
|
|
$found = true;
|
|
$value = A::pget_s($config, $pkey);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ($found) break;
|
|
}
|
|
}
|
|
|
|
if ($found) {
|
|
## résoudre les objets
|
|
if (is_array($value)) $this->resolveObjects($value, $pkey, $inProfile);
|
|
## et les références
|
|
if ($value instanceof Ref) $value = $this->resolveRef($value, $pkey, $inProfile);
|
|
elseif (is_array($value)) $this->resolveRefs($value, $pkey, $inProfile);
|
|
} else {
|
|
$value = $default;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
function getValue(string $pkey, $default=null, ?string $inProfile=null) {
|
|
if ($inProfile === null) $inProfile = $this->getProfile();
|
|
|
|
# la valeur est-elle déjà en cache?
|
|
if ($this->cacheHas($pkey, $inProfile)) {
|
|
return $this->cacheGet($pkey, $inProfile);
|
|
}
|
|
|
|
# vérifier la validité du profil
|
|
$profiles = $this->getProfiles();
|
|
if (!in_array($inProfile, $profiles)) {
|
|
# si le profil est invalide, prendre le profil par défaut
|
|
$inProfile = self::PROFILE_ALL;
|
|
}
|
|
|
|
if (substr($pkey, 0, 5) === "conf.") {
|
|
$confkey = substr($pkey, 5);
|
|
$value = $this->_getValue("user.$confkey", null, $inProfile);
|
|
if ($value === null) $value = $this->_getValue("app.$confkey", $default, $inProfile);
|
|
} elseif ($pkey === "conf") {
|
|
$value = $this->_getValue("app", null, $inProfile);
|
|
A::merge($value, $this->_getValue("user", null, $inProfile));
|
|
} else {
|
|
$value = $this->_getValue($pkey, $default, $inProfile);
|
|
}
|
|
|
|
## et pour finir, mettre la valeur en cache si nécessaire
|
|
$this->cacheSet($pkey, $value, $inProfile);
|
|
return $value;
|
|
}
|
|
|
|
protected $facts = [];
|
|
|
|
function setFact(string $fact, $value=true): void {
|
|
$this->facts[$fact] = $value;
|
|
}
|
|
|
|
function getFact(string $fact, $default=null) {
|
|
return A::get($this->facts, $fact, $default);
|
|
}
|
|
|
|
function isFact(string $fact, $value=true): bool {
|
|
return A::get($this->facts, $fact) === $value;
|
|
}
|
|
|
|
function isDebug(): bool {
|
|
$debug = defined("DEBUG")? true: null;
|
|
if ($debug === null) {
|
|
$DEBUG = getenv("DEBUG");
|
|
$debug = $DEBUG !== false? $DEBUG: null;
|
|
}
|
|
if ($debug === null) $debug = $this->getFact("debug");
|
|
if ($debug === null) $debug = $this->getValue("conf.debug");
|
|
return boolval($debug);
|
|
}
|
|
|
|
function setDebug(?bool $debug=true): void {
|
|
$this->setFact("debug");
|
|
}
|
|
}
|