nur-sery/nur_src/config/ConfigManager.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");
}
}