PocketMine-MP 5.15.1 git-fb9a74e8799c71ed8292cfa53abe7a4c9204629d
Config.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\utils;
25
27use Symfony\Component\Filesystem\Path;
28use function array_change_key_case;
29use function array_fill_keys;
30use function array_keys;
31use function array_shift;
32use function count;
33use function date;
34use function explode;
35use function file_exists;
36use function get_debug_type;
37use function implode;
38use function is_array;
39use function is_bool;
40use function json_decode;
41use function json_encode;
42use function preg_match_all;
43use function preg_replace;
44use function serialize;
45use function str_replace;
46use function strlen;
47use function strtolower;
48use function substr;
49use function trim;
50use function unserialize;
51use function yaml_emit;
52use function yaml_parse;
53use const CASE_LOWER;
54use const JSON_BIGINT_AS_STRING;
55use const JSON_PRETTY_PRINT;
56use const JSON_THROW_ON_ERROR;
57use const YAML_UTF8_ENCODING;
58
62class Config{
63 public const DETECT = -1; //Detect by file extension
64 public const PROPERTIES = 0; // .properties
65 public const CNF = Config::PROPERTIES; // .cnf
66 public const JSON = 1; // .js, .json
67 public const YAML = 2; // .yml, .yaml
68 //const EXPORT = 3; // .export, .xport
69 public const SERIALIZED = 4; // .sl
70 public const ENUM = 5; // .txt, .list, .enum
71 public const ENUMERATION = Config::ENUM;
72
77 private array $config = [];
78
83 private array $nestedCache = [];
84
85 private string $file;
86 private int $type = Config::DETECT;
87 private int $jsonOptions = JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING;
88
89 private bool $changed = false;
90
92 public static array $formats = [
93 "properties" => Config::PROPERTIES,
94 "cnf" => Config::CNF,
95 "conf" => Config::CNF,
96 "config" => Config::CNF,
97 "json" => Config::JSON,
98 "js" => Config::JSON,
99 "yml" => Config::YAML,
100 "yaml" => Config::YAML,
101 //"export" => Config::EXPORT,
102 //"xport" => Config::EXPORT,
103 "sl" => Config::SERIALIZED,
104 "serialize" => Config::SERIALIZED,
105 "txt" => Config::ENUM,
106 "list" => Config::ENUM,
107 "enum" => Config::ENUM
108 ];
109
116 public function __construct(string $file, int $type = Config::DETECT, array $default = []){
117 $this->load($file, $type, $default);
118 }
119
123 public function reload() : void{
124 $this->config = [];
125 $this->nestedCache = [];
126 $this->load($this->file, $this->type);
127 }
128
129 public function hasChanged() : bool{
130 return $this->changed;
131 }
132
133 public function setChanged(bool $changed = true) : void{
134 $this->changed = $changed;
135 }
136
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);
139 }
140
147 private function load(string $file, int $type = Config::DETECT, array $default = []) : void{
148 $this->file = $file;
149
150 $this->type = $type;
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];
155 }else{
156 throw new \InvalidArgumentException("Cannot detect config type of " . $this->file);
157 }
158 }
159
160 if(!file_exists($file)){
161 $this->config = $default;
162 $this->save();
163 }else{
164 $content = Filesystem::fileGetContents($this->file);
165 switch($this->type){
166 case Config::PROPERTIES:
167 $config = self::parseProperties($content);
168 break;
169 case Config::JSON:
170 try{
171 $config = json_decode($content, true, flags: JSON_THROW_ON_ERROR);
172 }catch(\JsonException $e){
173 throw ConfigLoadException::wrap($this->file, $e);
174 }
175 break;
176 case Config::YAML:
177 $content = self::fixYAMLIndexes($content);
178 try{
179 $config = ErrorToExceptionHandler::trap(fn() => yaml_parse($content));
180 }catch(\ErrorException $e){
181 throw ConfigLoadException::wrap($this->file, $e);
182 }
183 break;
184 case Config::SERIALIZED:
185 try{
186 $config = ErrorToExceptionHandler::trap(fn() => unserialize($content));
187 }catch(\ErrorException $e){
188 throw ConfigLoadException::wrap($this->file, $e);
189 }
190 break;
191 case Config::ENUM:
192 $config = array_fill_keys(self::parseList($content), true);
193 break;
194 default:
195 throw new \InvalidArgumentException("Invalid config type specified");
196 }
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));
199 }
200 $this->config = $config;
201 if($this->fillDefaults($default, $this->config) > 0){
202 $this->save();
203 }
204 }
205 }
206
210 public function getPath() : string{
211 return $this->file;
212 }
213
217 public function save() : void{
218 $content = null;
219 switch($this->type){
220 case Config::PROPERTIES:
221 $content = self::writeProperties($this->config);
222 break;
223 case Config::JSON:
224 $content = json_encode($this->config, $this->jsonOptions | JSON_THROW_ON_ERROR);
225 break;
226 case Config::YAML:
227 $content = yaml_emit($this->config, YAML_UTF8_ENCODING);
228 break;
229 case Config::SERIALIZED:
230 $content = serialize($this->config);
231 break;
232 case Config::ENUM:
233 $content = self::writeList(array_keys($this->config));
234 break;
235 default:
236 throw new AssumptionFailedError("Config type is unknown, has not been set or not detected");
237 }
238
239 Filesystem::safeFilePutContents($this->file, $content);
240
241 $this->changed = false;
242 }
243
251 public function setJsonOptions(int $options) : Config{
252 if($this->type !== Config::JSON){
253 throw new \RuntimeException("Attempt to set JSON options for non-JSON config");
254 }
255 $this->jsonOptions = $options;
256 $this->changed = true;
257
258 return $this;
259 }
260
268 public function enableJsonOption(int $option) : Config{
269 if($this->type !== Config::JSON){
270 throw new \RuntimeException("Attempt to enable JSON option for non-JSON config");
271 }
272 $this->jsonOptions |= $option;
273 $this->changed = true;
274
275 return $this;
276 }
277
285 public function disableJsonOption(int $option) : Config{
286 if($this->type !== Config::JSON){
287 throw new \RuntimeException("Attempt to disable JSON option for non-JSON config");
288 }
289 $this->jsonOptions &= ~$option;
290 $this->changed = true;
291
292 return $this;
293 }
294
301 public function getJsonOptions() : int{
302 if($this->type !== Config::JSON){
303 throw new \RuntimeException("Attempt to get JSON options for non-JSON config");
304 }
305 return $this->jsonOptions;
306 }
307
313 public function __get($k){
314 return $this->get($k);
315 }
316
321 public function __set($k, $v) : void{
322 $this->set($k, $v);
323 }
324
330 public function __isset($k){
331 return $this->exists($k);
332 }
333
337 public function __unset($k){
338 $this->remove($k);
339 }
340
341 public function setNested(string $key, mixed $value) : void{
342 $vars = explode(".", $key);
343 $base = array_shift($vars);
344
345 if(!isset($this->config[$base])){
346 $this->config[$base] = [];
347 }
348
349 $base = &$this->config[$base];
350
351 while(count($vars) > 0){
352 $baseKey = array_shift($vars);
353 if(!isset($base[$baseKey])){
354 $base[$baseKey] = [];
355 }
356 $base = &$base[$baseKey];
357 }
358
359 $base = $value;
360 $this->nestedCache = [];
361 $this->changed = true;
362 }
363
364 public function getNested(string $key, mixed $default = null) : mixed{
365 if(isset($this->nestedCache[$key])){
366 return $this->nestedCache[$key];
367 }
368
369 $vars = explode(".", $key);
370 $base = array_shift($vars);
371 if(isset($this->config[$base])){
372 $base = $this->config[$base];
373 }else{
374 return $default;
375 }
376
377 while(count($vars) > 0){
378 $baseKey = array_shift($vars);
379 if(is_array($base) && isset($base[$baseKey])){
380 $base = $base[$baseKey];
381 }else{
382 return $default;
383 }
384 }
385
386 return $this->nestedCache[$key] = $base;
387 }
388
389 public function removeNested(string $key) : void{
390 $this->nestedCache = [];
391 $this->changed = true;
392
393 $vars = explode(".", $key);
394
395 $currentNode = &$this->config;
396 while(count($vars) > 0){
397 $nodeName = array_shift($vars);
398 if(isset($currentNode[$nodeName])){
399 if(count($vars) === 0){ //final node
400 unset($currentNode[$nodeName]);
401 }elseif(is_array($currentNode[$nodeName])){
402 $currentNode = &$currentNode[$nodeName];
403 }
404 }else{
405 break;
406 }
407 }
408 }
409
410 public function get(string $k, mixed $default = false) : mixed{
411 return $this->config[$k] ?? $default;
412 }
413
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]);
420 }
421 }
422 }
423
428 public function setAll(array $v) : void{
429 $this->config = $v;
430 $this->changed = true;
431 }
432
436 public function exists(string $k, bool $lowercase = false) : bool{
437 if($lowercase){
438 $k = strtolower($k); //Convert requested key to lower
439 $array = array_change_key_case($this->config, CASE_LOWER); //Change all keys in array to lower
440 return isset($array[$k]); //Find $k in modified array
441 }else{
442 return isset($this->config[$k]);
443 }
444 }
445
446 public function remove(string $k) : void{
447 unset($this->config[$k]);
448 $this->changed = true;
449 }
450
455 public function getAll(bool $keys = false) : array{
456 return ($keys ? array_keys($this->config) : $this->config);
457 }
458
463 public function setDefaults(array $defaults) : void{
464 $this->fillDefaults($defaults, $this->config);
465 }
466
474 private function fillDefaults(array $default, array &$data) : int{
475 $changed = 0;
476 foreach(Utils::stringifyKeys($default) as $k => $v){
477 if(is_array($v)){
478 if(!isset($data[$k]) || !is_array($data[$k])){
479 $data[$k] = [];
480 }
481 $changed += $this->fillDefaults($v, $data[$k]);
482 }elseif(!isset($data[$k])){
483 $data[$k] = $v;
484 ++$changed;
485 }
486 }
487
488 if($changed > 0){
489 $this->changed = true;
490 }
491
492 return $changed;
493 }
494
499 public static function parseList(string $content) : array{
500 $result = [];
501 foreach(explode("\n", trim(str_replace("\r\n", "\n", $content))) as $v){
502 $v = trim($v);
503 if($v === ""){
504 continue;
505 }
506 $result[] = $v;
507 }
508 return $result;
509 }
510
515 public static function writeList(array $entries) : string{
516 return implode("\n", $entries);
517 }
518
523 public static function writeProperties(array $config) : string{
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){
526 if(is_bool($v)){
527 $v = $v ? "on" : "off";
528 }
529 $content .= $k . "=" . $v . "\r\n";
530 }
531
532 return $content;
533 }
534
539 public static function parseProperties(string $content) : array{
540 $result = [];
541 if(preg_match_all('/^\s*([a-zA-Z0-9\-_\.]+)[ \t]*=([^\r\n]*)/um', $content, $matches) > 0){ //false or 0 matches
542 foreach($matches[1] as $i => $k){
543 $v = trim($matches[2][$i]);
544 switch(strtolower($v)){
545 case "on":
546 case "true":
547 case "yes":
548 $v = true;
549 break;
550 case "off":
551 case "false":
552 case "no":
553 $v = false;
554 break;
555 default:
556 $v = match($v){
557 (string) ((int) $v) => (int) $v,
558 (string) ((float) $v) => (float) $v,
559 default => $v,
560 };
561 break;
562 }
563 $result[(string) $k] = $v;
564 }
565 }
566
567 return $result;
568 }
569}
__construct(string $file, int $type=Config::DETECT, array $default=[])
Definition: Config.php:116
static writeProperties(array $config)
Definition: Config.php:523
static writeList(array $entries)
Definition: Config.php:515
enableJsonOption(int $option)
Definition: Config.php:268
setDefaults(array $defaults)
Definition: Config.php:463
static parseProperties(string $content)
Definition: Config.php:539
exists(string $k, bool $lowercase=false)
Definition: Config.php:436
getAll(bool $keys=false)
Definition: Config.php:455
setJsonOptions(int $options)
Definition: Config.php:251
disableJsonOption(int $option)
Definition: Config.php:285
static parseList(string $content)
Definition: Config.php:499