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,