64 public const DETECT = -1;
65 public const PROPERTIES = 0;
66 public const CNF = Config::PROPERTIES;
67 public const JSON = 1;
68 public const YAML = 2;
70 public const SERIALIZED = 4;
71 public const ENUM = 5;
72 public const ENUMERATION = Config::ENUM;
78 private array $config = [];
84 private array $nestedCache = [];
87 private int $type = Config::DETECT;
88 private int $jsonOptions = JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING;
90 private bool $changed =
false;
93 public static array $formats = [
94 "properties" => Config::PROPERTIES,
96 "conf" => Config::CNF,
97 "config" => Config::CNF,
98 "json" => Config::JSON,
100 "yml" => Config::YAML,
101 "yaml" => Config::YAML,
104 "sl" => Config::SERIALIZED,
105 "serialize" => Config::SERIALIZED,
106 "txt" => Config::ENUM,
107 "list" => Config::ENUM,
108 "enum" => Config::ENUM
117 public function __construct(
string $file,
int $type = Config::DETECT, array $default = []){
118 $this->load($file, $type, $default);
126 $this->nestedCache = [];
127 $this->load($this->file, $this->type);
130 public function hasChanged() : bool{
131 return $this->changed;
134 public function setChanged(
bool $changed =
true) : void{
135 $this->changed = $changed;
138 public static function fixYAMLIndexes(
string $str) : string{
139 return preg_replace(
"#^( *)(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)( *)\:#m",
"$1\"$2\"$3:", $str);
148 private function load(
string $file,
int $type = Config::DETECT, array $default = []) : void{
152 if($this->type === Config::DETECT){
153 $extension = strtolower(Path::getExtension($this->file));
154 if(isset(Config::$formats[$extension])){
155 $this->type = Config::$formats[$extension];
157 throw new \InvalidArgumentException(
"Cannot detect config type of " . $this->file);
161 if(!file_exists($file)){
162 $this->config = $default;
165 $content = Filesystem::fileGetContents($this->file);
167 case Config::PROPERTIES:
168 $config = self::parseProperties($content);
172 $config = json_decode($content,
true, flags: JSON_THROW_ON_ERROR);
173 }
catch(\JsonException $e){
174 throw ConfigLoadException::wrap($this->file, $e);
178 $content = self::fixYAMLIndexes($content);
180 $config = ErrorToExceptionHandler::trap(fn() => yaml_parse($content));
181 }
catch(\ErrorException $e){
182 throw ConfigLoadException::wrap($this->file, $e);
185 case Config::SERIALIZED:
187 $config = ErrorToExceptionHandler::trap(fn() => unserialize($content));
188 }
catch(\ErrorException $e){
189 throw ConfigLoadException::wrap($this->file, $e);
193 $config = array_fill_keys(self::parseList($content),
true);
196 throw new \InvalidArgumentException(
"Invalid config type specified");
198 if(!is_array($config)){
199 throw new ConfigLoadException(
"Failed to load config $this->file: Expected array for base type, but got " . get_debug_type($config));
201 $this->config = $config;
202 if($this->fillDefaults($default, $this->config) > 0){
221 case Config::PROPERTIES:
222 $content = self::writeProperties($this->config);
225 $content = json_encode($this->config, $this->jsonOptions | JSON_THROW_ON_ERROR);
228 $content = yaml_emit($this->config, YAML_UTF8_ENCODING);
230 case Config::SERIALIZED:
231 $content = serialize($this->config);
234 $content = self::writeList(array_keys($this->config));
240 Filesystem::safeFilePutContents($this->file, $content);
242 $this->changed =
false;
253 if($this->type !==
Config::JSON){
254 throw new \RuntimeException(
"Attempt to set JSON options for non-JSON config");
256 $this->jsonOptions = $options;
257 $this->changed =
true;
270 if($this->type !==
Config::JSON){
271 throw new \RuntimeException(
"Attempt to enable JSON option for non-JSON config");
273 $this->jsonOptions |= $option;
274 $this->changed =
true;
287 if($this->type !==
Config::JSON){
288 throw new \RuntimeException(
"Attempt to disable JSON option for non-JSON config");
290 $this->jsonOptions &= ~$option;
291 $this->changed =
true;
303 if($this->type !==
Config::JSON){
304 throw new \RuntimeException(
"Attempt to get JSON options for non-JSON config");
306 return $this->jsonOptions;
315 return $this->
get($k);
322 public function __set($k, $v) : void{
332 return $this->exists($k);
342 public function setNested(
string $key, mixed $value) : void{
343 $vars = explode(
".", $key, limit: PHP_INT_MAX);
344 $base = array_shift($vars);
346 if(!isset($this->config[$base])){
347 $this->config[$base] = [];
350 $base = &$this->config[$base];
352 while(count($vars) > 0){
353 $baseKey = array_shift($vars);
354 if(!isset($base[$baseKey])){
355 $base[$baseKey] = [];
357 $base = &$base[$baseKey];
361 $this->nestedCache = [];
362 $this->changed =
true;
365 public function getNested(
string $key, mixed $default =
null) : mixed{
366 if(isset($this->nestedCache[$key])){
367 return $this->nestedCache[$key];
370 $vars = explode(
".", $key, limit: PHP_INT_MAX);
371 $base = array_shift($vars);
372 if(isset($this->config[$base])){
373 $base = $this->config[$base];
378 while(count($vars) > 0){
379 $baseKey = array_shift($vars);
380 if(is_array($base) && isset($base[$baseKey])){
381 $base = $base[$baseKey];
387 return $this->nestedCache[$key] = $base;
390 public function removeNested(
string $key) : void{
391 $this->nestedCache = [];
392 $this->changed =
true;
394 $vars = explode(
".", $key, limit: PHP_INT_MAX);
396 $currentNode = &$this->config;
397 while(count($vars) > 0){
398 $nodeName = array_shift($vars);
399 if(isset($currentNode[$nodeName])){
400 if(count($vars) === 0){
401 unset($currentNode[$nodeName]);
402 }elseif(is_array($currentNode[$nodeName])){
403 $currentNode = &$currentNode[$nodeName];
411 public function get(
string $k, mixed $default =
false) : mixed{
412 return $this->config[$k] ?? $default;
415 public function set(
string $k, mixed $v =
true) : void{
416 $this->config[$k] = $v;
417 $this->changed =
true;
418 foreach(Utils::stringifyKeys($this->nestedCache) as $nestedKey => $nvalue){
419 if(substr($nestedKey, 0, strlen($k) + 1) === ($k .
".")){
420 unset($this->nestedCache[$nestedKey]);
431 $this->changed =
true;
437 public function exists(
string $k,
bool $lowercase =
false) : bool{
440 $array = array_change_key_case($this->config, CASE_LOWER);
441 return isset($array[$k]);
443 return isset($this->config[$k]);
447 public function remove(
string $k) : void{
448 unset($this->config[$k]);
449 $this->changed =
true;
456 public function getAll(
bool $keys =
false) : array{
457 return ($keys ? array_keys($this->config) : $this->config);
464 $this->fillDefaults($defaults, $this->config);
472 private function fillDefaults(array $default, array &$data) : int{
474 foreach(Utils::promoteKeys($default) as $k => $v){
476 if(!isset($data[$k]) || !is_array($data[$k])){
479 $changed += $this->fillDefaults($v, $data[$k]);
480 }elseif(!isset($data[$k])){
487 $this->changed =
true;
497 public static function parseList(
string $content) : array{
499 foreach(explode(
"\n", trim(str_replace(
"\r\n",
"\n", $content)), limit: PHP_INT_MAX) as $v){
513 public static function writeList(array $entries) : string{
514 return implode(
"\n", $entries);
522 $content =
"#Properties Config file\r\n#" . date(
"D M j H:i:s T Y") .
"\r\n";
523 foreach(Utils::promoteKeys($config) as $k => $v){
525 $v = $v ?
"on" :
"off";
527 $content .= $k .
"=" . $v .
"\r\n";
539 if(preg_match_all(
'/^\s*([a-zA-Z0-9\-_\.]+)[ \t]*=([^\r\n]*)/um', $content, $matches) > 0){
540 foreach($matches[1] as $i => $k){
541 $v = trim($matches[2][$i]);
542 switch(strtolower($v)){
555 (string) ((
int) $v) => (int) $v,
556 (
string) ((float) $v) => (
float) $v,