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"); } }