63 public const DETECT = -1;
64 public const PROPERTIES = 0;
65 public const CNF = Config::PROPERTIES;
66 public const JSON = 1;
67 public const YAML = 2;
69 public const SERIALIZED = 4;
70 public const ENUM = 5;
71 public const ENUMERATION = Config::ENUM;
77 private array $config = [];
83 private array $nestedCache = [];
86 private int $type = Config::DETECT;
87 private int $jsonOptions = JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING;
89 private bool $changed =
false;
92 public static array $formats = [
93 "properties" => Config::PROPERTIES,
95 "conf" => Config::CNF,
96 "config" => Config::CNF,
97 "json" => Config::JSON,
99 "yml" => Config::YAML,
100 "yaml" => Config::YAML,
103 "sl" => Config::SERIALIZED,
104 "serialize" => Config::SERIALIZED,
105 "txt" => Config::ENUM,
106 "list" => Config::ENUM,
107 "enum" => Config::ENUM
116 public function __construct(
string $file,
int $type = Config::DETECT, array $default = []){
117 $this->load($file, $type, $default);
125 $this->nestedCache = [];
126 $this->load($this->file, $this->type);
129 public function hasChanged() : bool{
130 return $this->changed;
133 public function setChanged(
bool $changed =
true) : void{
134 $this->changed = $changed;
137 public static function fixYAMLIndexes(
string $str) : string{
138 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);
147 private function load(
string $file,
int $type = Config::DETECT, array $default = []) : void{
151 if($this->type === Config::DETECT){
152 $extension = strtolower(Path::getExtension($this->file));
153 if(isset(Config::$formats[$extension])){
154 $this->type = Config::$formats[$extension];
156 throw new \InvalidArgumentException(
"Cannot detect config type of " . $this->file);
160 if(!file_exists($file)){
161 $this->config = $default;
164 $content = Filesystem::fileGetContents($this->file);
166 case Config::PROPERTIES:
167 $config = self::parseProperties($content);
171 $config = json_decode($content,
true, flags: JSON_THROW_ON_ERROR);
172 }
catch(\JsonException $e){
173 throw ConfigLoadException::wrap($this->file, $e);
177 $content = self::fixYAMLIndexes($content);
179 $config = ErrorToExceptionHandler::trap(fn() => yaml_parse($content));
180 }
catch(\ErrorException $e){
181 throw ConfigLoadException::wrap($this->file, $e);
184 case Config::SERIALIZED:
186 $config = ErrorToExceptionHandler::trap(fn() => unserialize($content));
187 }
catch(\ErrorException $e){
188 throw ConfigLoadException::wrap($this->file, $e);
192 $config = array_fill_keys(self::parseList($content),
true);
195 throw new \InvalidArgumentException(
"Invalid config type specified");
197 if(!is_array($config)){
198 throw new ConfigLoadException(
"Failed to load config $this->file: Expected array for base type, but got " . get_debug_type($config));
200 $this->config = $config;
201 if($this->fillDefaults($default, $this->config) > 0){
220 case Config::PROPERTIES:
221 $content = self::writeProperties($this->config);
224 $content = json_encode($this->config, $this->jsonOptions | JSON_THROW_ON_ERROR);
227 $content = yaml_emit($this->config, YAML_UTF8_ENCODING);
229 case Config::SERIALIZED:
230 $content = serialize($this->config);
233 $content = self::writeList(array_keys($this->config));
239 Filesystem::safeFilePutContents($this->file, $content);
241 $this->changed =
false;
252 if($this->type !==
Config::JSON){
253 throw new \RuntimeException(
"Attempt to set JSON options for non-JSON config");
255 $this->jsonOptions = $options;
256 $this->changed =
true;
269 if($this->type !==
Config::JSON){
270 throw new \RuntimeException(
"Attempt to enable JSON option for non-JSON config");
272 $this->jsonOptions |= $option;
273 $this->changed =
true;
286 if($this->type !==
Config::JSON){
287 throw new \RuntimeException(
"Attempt to disable JSON option for non-JSON config");
289 $this->jsonOptions &= ~$option;
290 $this->changed =
true;
302 if($this->type !==
Config::JSON){
303 throw new \RuntimeException(
"Attempt to get JSON options for non-JSON config");
305 return $this->jsonOptions;
314 return $this->
get($k);
321 public function __set($k, $v) : void{
331 return $this->exists($k);
341 public function setNested(
string $key, mixed $value) : void{
342 $vars = explode(
".", $key);
343 $base = array_shift($vars);
345 if(!isset($this->config[$base])){
346 $this->config[$base] = [];
349 $base = &$this->config[$base];
351 while(count($vars) > 0){
352 $baseKey = array_shift($vars);
353 if(!isset($base[$baseKey])){
354 $base[$baseKey] = [];
356 $base = &$base[$baseKey];
360 $this->nestedCache = [];
361 $this->changed =
true;
364 public function getNested(
string $key, mixed $default =
null) : mixed{
365 if(isset($this->nestedCache[$key])){
366 return $this->nestedCache[$key];
369 $vars = explode(
".", $key);
370 $base = array_shift($vars);
371 if(isset($this->config[$base])){
372 $base = $this->config[$base];
377 while(count($vars) > 0){
378 $baseKey = array_shift($vars);
379 if(is_array($base) && isset($base[$baseKey])){
380 $base = $base[$baseKey];
386 return $this->nestedCache[$key] = $base;
389 public function removeNested(
string $key) : void{
390 $this->nestedCache = [];
391 $this->changed =
true;
393 $vars = explode(
".", $key);
395 $currentNode = &$this->config;
396 while(count($vars) > 0){
397 $nodeName = array_shift($vars);
398 if(isset($currentNode[$nodeName])){
399 if(count($vars) === 0){
400 unset($currentNode[$nodeName]);
401 }elseif(is_array($currentNode[$nodeName])){
402 $currentNode = &$currentNode[$nodeName];
410 public function get(
string $k, mixed $default =
false) : mixed{
411 return $this->config[$k] ?? $default;
414 public function set(
string $k, mixed $v =
true) : void{
415 $this->config[$k] = $v;
416 $this->changed =
true;
417 foreach(Utils::stringifyKeys($this->nestedCache) as $nestedKey => $nvalue){
418 if(substr($nestedKey, 0, strlen($k) + 1) === ($k .
".")){
419 unset($this->nestedCache[$nestedKey]);
430 $this->changed =
true;
436 public function exists(
string $k,
bool $lowercase =
false) : bool{
439 $array = array_change_key_case($this->config, CASE_LOWER);
440 return isset($array[$k]);
442 return isset($this->config[$k]);
446 public function remove(
string $k) : void{
447 unset($this->config[$k]);
448 $this->changed =
true;
455 public function getAll(
bool $keys =
false) : array{
456 return ($keys ? array_keys($this->config) : $this->config);
464 $this->fillDefaults($defaults, $this->config);
474 private function fillDefaults(array $default, array &$data) : int{
476 foreach(Utils::stringifyKeys($default) as $k => $v){
478 if(!isset($data[$k]) || !is_array($data[$k])){
481 $changed += $this->fillDefaults($v, $data[$k]);
482 }elseif(!isset($data[$k])){
489 $this->changed =
true;
499 public static function parseList(
string $content) : array{
501 foreach(explode(
"\n", trim(str_replace(
"\r\n",
"\n", $content))) as $v){
515 public static function writeList(array $entries) : string{
516 return implode(
"\n", $entries);
524 $content =
"#Properties Config file\r\n#" . date(
"D M j H:i:s T Y") .
"\r\n";
525 foreach(Utils::stringifyKeys($config) as $k => $v){
527 $v = $v ?
"on" :
"off";
529 $content .= $k .
"=" . $v .
"\r\n";
541 if(preg_match_all(
'/^\s*([a-zA-Z0-9\-_\.]+)[ \t]*=([^\r\n]*)/um', $content, $matches) > 0){
542 foreach($matches[1] as $i => $k){
543 $v = trim($matches[2][$i]);
544 switch(strtolower($v)){
557 (string) ((
int) $v) => (int) $v,
558 (
string) ((float) $v) => (
float) $v,
563 $result[(string) $k] = $v;