PocketMine-MP 5.27.1 git-9af3cde03fabbe4129c79e46dc87ffa0fff446e6
Loading...
Searching...
No Matches
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 PHP_INT_MAX;
58use const YAML_UTF8_ENCODING;
59
63class Config{
64 public const DETECT = -1; //Detect by file extension
65 public const PROPERTIES = 0; // .properties
66 public const CNF = Config::PROPERTIES; // .cnf
67 public const JSON = 1; // .js, .json
68 public const YAML = 2; // .yml, .yaml
69 //const EXPORT = 3; // .export, .xport
70 public const SERIALIZED = 4; // .sl
71 public const ENUM = 5; // .txt, .list, .enum
72 public const ENUMERATION = Config::ENUM;
73
78 private array $config = [];
79
84 private array $nestedCache = [];
85
86 private string $file;
87 private int $type = Config::DETECT;
88 private int $jsonOptions = JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING;
89
90 private bool $changed = false;
91
93 public static array $formats = [
94 "properties" => Config::PROPERTIES,
95 "cnf" => Config::CNF,
96 "conf" => Config::CNF,
97 "config" => Config::CNF,
98 "json" => Config::JSON,
99 "js" => Config::JSON,
100 "yml" => Config::YAML,
101 "yaml" => Config::YAML,
102 //"export" => Config::EXPORT,
103 //"xport" => Config::EXPORT,
104 "sl" => Config::SERIALIZED,
105 "serialize" => Config::SERIALIZED,
106 "txt" => Config::ENUM,
107 "list" => Config::ENUM,
108 "enum" => Config::ENUM
109 ];
110
117 public function __construct(string $file, int $type = Config::DETECT, array $default = []){
118 $this->load($file, $type, $default);
119 }
120
124 public function reload() : void{
125 $this->config = [];
126 $this->nestedCache = [];
127 $this->load($this->file, $this->type);
128 }
129
130 public function hasChanged() : bool{
131 return $this->changed;
132 }
133
134 public function setChanged(bool $changed = true) : void{
135 $this->changed = $changed;
136 }
137
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);
140 }
141
148 private function load(string $file, int $type = Config::DETECT, array $default = []) : void{
149 $this->file = $file;
150
151 $this->type = $type;
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];
156 }else{
157 throw new \InvalidArgumentException("Cannot detect config type of " . $this->file);
158 }
159 }
160
161 if(!file_exists($file)){
162 $this->config = $default;
163 $this->save();
164 }else{
165 $content = Filesystem::fileGetContents($this->file);
166 switch($this->type){
167 case Config::PROPERTIES:
168 $config = self::parseProperties($content);
169 break;
170 case Config::JSON:
171 try{
172 $config = json_decode($content, true, flags: JSON_THROW_ON_ERROR);
173 }catch(\JsonException $e){
174 throw ConfigLoadException::wrap($this->file, $e);
175 }
176 break;
177 case Config::YAML:
178 $content = self::fixYAMLIndexes($content);
179 try{
180 $config = ErrorToExceptionHandler::trap(fn() => yaml_parse($content));
181 }catch(\ErrorException $e){
182 throw ConfigLoadException::wrap($this->file, $e);
183 }
184 break;
185 case Config::SERIALIZED:
186 try{
187 $config = ErrorToExceptionHandler::trap(fn() => unserialize($content));
188 }catch(\ErrorException $e){
189 throw ConfigLoadException::wrap($this->file, $e);
190 }
191 break;
192 case Config::ENUM:
193 $config = array_fill_keys(self::parseList($content), true);
194 break;
195 default:
196 throw new \InvalidArgumentException("Invalid config type specified");
197 }
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));
200 }
201 $this->config = $config;
202 if($this->fillDefaults($default, $this->config) > 0){
203 $this->save();
204 }
205 }
206 }
207
211 public function getPath() : string{
212 return $this->file;
213 }
214
218 public function save() : void{
219 $content = null;
220 switch($this->type){
221 case Config::PROPERTIES:
222 $content = self::writeProperties($this->config);
223 break;
224 case Config::JSON:
225 $content = json_encode($this->config, $this->jsonOptions | JSON_THROW_ON_ERROR);
226 break;
227 case Config::YAML:
228 $content = yaml_emit($this->config, YAML_UTF8_ENCODING);
229 break;
230 case Config::SERIALIZED:
231 $content = serialize($this->config);
232 break;
233 case Config::ENUM:
234 $content = self::writeList(array_keys($this->config));
235 break;
236 default:
237 throw new AssumptionFailedError("Config type is unknown, has not been set or not detected");
238 }
239
240 Filesystem::safeFilePutContents($this->file, $content);
241
242 $this->changed = false;
243 }
244
252 public function setJsonOptions(int $options) : Config{
253 if($this->type !== Config::JSON){
254 throw new \RuntimeException("Attempt to set JSON options for non-JSON config");
255 }
256 $this->jsonOptions = $options;
257 $this->changed = true;
258
259 return $this;
260 }
261
269 public function enableJsonOption(int $option) : Config{
270 if($this->type !== Config::JSON){
271 throw new \RuntimeException("Attempt to enable JSON option for non-JSON config");
272 }
273 $this->jsonOptions |= $option;
274 $this->changed = true;
275
276 return $this;
277 }
278
286 public function disableJsonOption(int $option) : Config{
287 if($this->type !== Config::JSON){
288 throw new \RuntimeException("Attempt to disable JSON option for non-JSON config");
289 }
290 $this->jsonOptions &= ~$option;
291 $this->changed = true;
292
293 return $this;
294 }
295
302 public function getJsonOptions() : int{
303 if($this->type !== Config::JSON){
304 throw new \RuntimeException("Attempt to get JSON options for non-JSON config");
305 }
306 return $this->jsonOptions;
307 }
308
314 public function __get($k){
315 return $this->get($k);
316 }
317
322 public function __set($k, $v) : void{
323 $this->set($k, $v);
324 }
325
331 public function __isset($k){
332 return $this->exists($k);
333 }
334
338 public function __unset($k){
339 $this->remove($k);
340 }
341
342 public function setNested(string $key, mixed $value) : void{
343 $vars = explode(".", $key, limit: PHP_INT_MAX);
344 $base = array_shift($vars);
345
346 if(!isset($this->config[$base])){
347 $this->config[$base] = [];
348 }
349
350 $base = &$this->config[$base];
351
352 while(count($vars) > 0){
353 $baseKey = array_shift($vars);
354 if(!isset($base[$baseKey])){
355 $base[$baseKey] = [];
356 }
357 $base = &$base[$baseKey];
358 }
359
360 $base = $value;
361 $this->nestedCache = [];
362 $this->changed = true;
363 }
364
365 public function getNested(string $key, mixed $default = null) : mixed{
366 if(isset($this->nestedCache[$key])){
367 return $this->nestedCache[$key];
368 }
369
370 $vars = explode(".", $key, limit: PHP_INT_MAX);
371 $base = array_shift($vars);
372 if(isset($this->config[$base])){
373 $base = $this->config[$base];
374 }else{
375 return $default;
376 }
377
378 while(count($vars) > 0){
379 $baseKey = array_shift($vars);
380 if(is_array($base) && isset($base[$baseKey])){
381 $base = $base[$baseKey];
382 }else{
383 return $default;
384 }
385 }
386
387 return $this->nestedCache[$key] = $base;
388 }
389
390 public function removeNested(string $key) : void{
391 $this->nestedCache = [];
392 $this->changed = true;
393
394 $vars = explode(".", $key, limit: PHP_INT_MAX);
395
396 $currentNode = &$this->config;
397 while(count($vars) > 0){
398 $nodeName = array_shift($vars);
399 if(isset($currentNode[$nodeName])){
400 if(count($vars) === 0){ //final node
401 unset($currentNode[$nodeName]);
402 }elseif(is_array($currentNode[$nodeName])){
403 $currentNode = &$currentNode[$nodeName];
404 }
405 }else{
406 break;
407 }
408 }
409 }
410
411 public function get(string $k, mixed $default = false) : mixed{
412 return $this->config[$k] ?? $default;
413 }
414
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]);
421 }
422 }
423 }
424
429 public function setAll(array $v) : void{
430 $this->config = $v;
431 $this->changed = true;
432 }
433
437 public function exists(string $k, bool $lowercase = false) : bool{
438 if($lowercase){
439 $k = strtolower($k); //Convert requested key to lower
440 $array = array_change_key_case($this->config, CASE_LOWER); //Change all keys in array to lower
441 return isset($array[$k]); //Find $k in modified array
442 }else{
443 return isset($this->config[$k]);
444 }
445 }
446
447 public function remove(string $k) : void{
448 unset($this->config[$k]);
449 $this->changed = true;
450 }
451
456 public function getAll(bool $keys = false) : array{
457 return ($keys ? array_keys($this->config) : $this->config);
458 }
459
463 public function setDefaults(array $defaults) : void{
464 $this->fillDefaults($defaults, $this->config);
465 }
466
472 private function fillDefaults(array $default, array &$data) : int{
473 $changed = 0;
474 foreach(Utils::promoteKeys($default) as $k => $v){
475 if(is_array($v)){
476 if(!isset($data[$k]) || !is_array($data[$k])){
477 $data[$k] = [];
478 }
479 $changed += $this->fillDefaults($v, $data[$k]);
480 }elseif(!isset($data[$k])){
481 $data[$k] = $v;
482 ++$changed;
483 }
484 }
485
486 if($changed > 0){
487 $this->changed = true;
488 }
489
490 return $changed;
491 }
492
497 public static function parseList(string $content) : array{
498 $result = [];
499 foreach(explode("\n", trim(str_replace("\r\n", "\n", $content)), limit: PHP_INT_MAX) as $v){
500 $v = trim($v);
501 if($v === ""){
502 continue;
503 }
504 $result[] = $v;
505 }
506 return $result;
507 }
508
513 public static function writeList(array $entries) : string{
514 return implode("\n", $entries);
515 }
516
521 public static function writeProperties(array $config) : string{
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){
524 if(is_bool($v)){
525 $v = $v ? "on" : "off";
526 }
527 $content .= $k . "=" . $v . "\r\n";
528 }
529
530 return $content;
531 }
532
537 public static function parseProperties(string $content) : array{
538 $result = [];
539 if(preg_match_all('/^\s*([a-zA-Z0-9\-_\.]+)[ \t]*=([^\r\n]*)/um', $content, $matches) > 0){ //false or 0 matches
540 foreach($matches[1] as $i => $k){
541 $v = trim($matches[2][$i]);
542 switch(strtolower($v)){
543 case "on":
544 case "true":
545 case "yes":
546 $v = true;
547 break;
548 case "off":
549 case "false":
550 case "no":
551 $v = false;
552 break;
553 default:
554 $v = match($v){
555 (string) ((int) $v) => (int) $v,
556 (string) ((float) $v) => (float) $v,
557 default => $v,
558 };
559 break;
560 }
561 $result[$k] = $v;
562 }
563 }
564
565 return $result;
566 }
567}
__construct(string $file, int $type=Config::DETECT, array $default=[])
Definition Config.php:117
static writeProperties(array $config)
Definition Config.php:521
static writeList(array $entries)
Definition Config.php:513
enableJsonOption(int $option)
Definition Config.php:269
setDefaults(array $defaults)
Definition Config.php:463
static parseProperties(string $content)
Definition Config.php:537
exists(string $k, bool $lowercase=false)
Definition Config.php:437
getAll(bool $keys=false)
Definition Config.php:456
setJsonOptions(int $options)
Definition Config.php:252
disableJsonOption(int $option)
Definition Config.php:286
static parseList(string $content)
Definition Config.php:497