forkbb/app/Core/Config.php
2023-11-22 23:21:30 +07:00

510 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* This file is part of the ForkBB <https://github.com/forkbb>.
*
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
declare(strict_types=1);
namespace ForkBB\Core;
use ForkBB\Core\Exceptions\ForkException;
class Config
{
/**
* Путь до файла конфига
*/
protected string $path;
/**
* Содержимое файла конфига
*/
protected string $fileContents;
/**
* Начальная позиция массива конфига
*/
protected int $arrayStartPos;
/**
* Массив токенов
*/
protected array $tokens;
/**
* Текущая позиция в массиве токенов
*/
protected int $position;
/**
* Массив полученый из файла настройки путем его парсинга
*/
protected array $configArray;
/**
* Строка массива конфига в файле конфигурации
*/
protected string $configStr;
public function __construct(string $path)
{
if (! \is_file($path)) {
throw new ForkException('Config not found');
}
if (! \is_readable($path)) {
throw new ForkException('Config can not be read');
}
if (! \is_writable($path)) {
throw new ForkException('Config can not be write');
}
$this->fileContents = \file_get_contents($path);
$this->path = $path;
if (\preg_match('%\[\s*\'BASE_URL\'\s+=>%s', $this->fileContents, $matches, \PREG_OFFSET_CAPTURE)) {
$this->arrayStartPos = $matches[0][1];
$this->configArray = $this->getArray();
return;
}
throw new ForkException('The structure of the config file is undefined');
}
/**
* Получает массив настроек из файла конфига
*/
protected function getArray(): array
{
if (
false === \preg_match_all(
'%
//[^\r\n]*+
|
\#[^\r\n]*+
|
/\*.*?\*/
|
\'.*?(?<!\\\\)\'
|
".*?(?<!\\\\)"
|
\s+
|
\[
|
\]
|
,
|
=>
|
function\s*\(.+?\)\s*\{.*?\}(?=,)
|
(?:\\\\)?[\w-]+\s*\(.+?\)(?=,)
|
\S+(?<![,\]\)])
%sx',
\substr($this->fileContents, $this->arrayStartPos),
$matches
)
|| empty($matches)
) {
throw new ForkException('Config array cannot be parsed (1)');
}
$this->tokens = $matches[0];
$this->position = 0;
$this->configStr = '';
return $this->parse('ZERO');
}
/**
* Очищает ключ от кавычек
*/
protected function clearKey(mixed $key): string
{
if (! \is_string($key)) {
throw new ForkException('Config array cannot be parsed (2)');
}
if ((
'\'' === $key[0]
&& \strlen($key) > 1
&& '\'' === $key[-1]
)
|| (
'"' === $key[0]
&& \strlen($key) > 1
&& '"' === $key[-1]
)
) {
return \substr($key, 1, -1);
}
return $key;
}
/**
* Создает массив конфига из токенов (массива подстрок)
*/
protected function parse(string $type): array
{
$result = [];
$value = null;
$key = null;
$other = '';
$value_before = '';
$value_after = '';
$key_before = '';
$key_after = '';
while (isset($this->tokens[$this->position])) {
$token = $this->tokens[$this->position];
$this->configStr .= $token;
// открытие массива
if ('[' === $token) {
switch ($type) {
case 'ZERO':
$type = 'NEW';
break;
case 'NEW':
case '=>':
$this->configStr = \substr($this->configStr, 0, -1);
$value = $this->parse('ZERO');
$value_before = $other;
$other = '';
$type = 'VALUE';
break;
default:
throw new ForkException('Config array cannot be parsed (3)');
}
// закрытие массива
} elseif (']' === $token) {
switch ($type) {
case 'NEW':
case 'VALUE':
case 'VALUE_OR_KEY':
if (null !== $value) {
$value = [
'value' => $value,
'value_before' => $value_before,
'value_after' => $other,
'key_before' => $key_before,
'key_after' => $key_after,
];
if (null !== $key) {
$result[$this->clearKey($key)] = $value;
} else {
$result[] = $value;
}
} elseif (null !== $key) {
throw new ForkException('Config array cannot be parsed (4)');
}
return $result;
default:
throw new ForkException('Config array cannot be parsed (5)');
}
// новый элемент
} elseif (',' === $token) {
switch ($type) {
case 'VALUE':
case 'VALUE_OR_KEY':
$type = 'NEW';
break;
default:
throw new ForkException('Config array cannot be parsed (6)');
}
// присвоение значения
} elseif ('=>' === $token) {
switch ($type) {
case 'VALUE_OR_KEY':
$key = $value;
$key_before = $value_before;
$key_after = $other;
$other = '';
$value = null;
$value_before = '';
$type = '=>';
break;
default:
throw new ForkException('Config array cannot be parsed (7)');
}
// пробел, комментарий
} elseif (
'' === \trim($token)
|| 0 === \strpos($token, '//')
|| 0 === \strpos($token, '/*')
|| '#' === $token[0]
) {
switch ($type) {
case 'NEW':
case 'VALUE_OR_KEY':
case 'VALUE':
case '=>':
$other .= $token;
break;
default:
throw new ForkException('Config array cannot be parsed (8)');
}
// какое-то значение
} else {
switch ($type) {
case 'NEW':
if (null !== $value) {
\preg_match('%^([^\r\n]*+)(.*)$%s', $other, $matches);
$value_after = $matches[1];
$other = $matches[2];
$value = [
'value' => $value,
'value_before' => $value_before,
'value_after' => $value_after,
'key_before' => $key_before,
'key_after' => $key_after,
];
$value_before = '';
$value_after = '';
$key_before = '';
$key_after = '';
if (null !== $key) {
$result[$this->clearKey($key)] = $value;
} else {
$result[] = $value;
}
$value = null;
$key = null;
} elseif (null !== $key) {
throw new ForkException('Config array cannot be parsed (9)');
}
$type = 'VALUE_OR_KEY';
break;
case '=>':
$type = 'VALUE';
break;
default:
throw new ForkException('Config array cannot be parsed (10)');
}
$value = $token;
$value_before = $other;
$other = '';
}
++$this->position;
}
}
protected function isFormat(mixed $data): bool
{
return \is_array($data)
&& \array_key_exists('value', $data)
&& \array_key_exists('value_before', $data)
&& \array_key_exists('value_after', $data)
&& \array_key_exists('key_before', $data)
&& \array_key_exists('key_after', $data);
}
/**
* Добавляет/заменяет элемент в конфиг(е)
*/
public function add(string $path, mixed $value, string $after = null): bool
{
if (empty($this->configArray)) {
$this->configArray = $this->getArray();
}
$pathArray = \explode('=>', $path);
$size = \count($pathArray);
$i = 0;
$config = &$this->configArray;
while ($i < $size - 1) {
$key = $pathArray[$i];
if (\is_numeric($key)) { //???? O_o
$config[] = [];
$config = &$config[\array_key_last($config)];
} else {
$config[$key] ??= [];
if ($this->isFormat($config[$key])) {
$config = &$config[$key]['value'];
} else {
$config = &$config[$key];
}
}
++$i;
}
$key = $pathArray[$i];
if (
\is_numeric($key) //???? O_o
|| \is_numeric($after) //???? O_o O_o O_o
) {
$config[] = $value;
} elseif (isset($config[$key])) {
if (
$this->isFormat($config[$key])
&& ! $this->isFormat($value)
) {
$config[$key]['value'] = $value;
} else {
$config[$key] = $value;
}
} elseif (
null === $after
|| ! isset($config[$after])
) {
$config[$key] = $value;
} else {
$new = [];
foreach ($config as $k => $v) {
if (\is_int($k)) {
$new[] = $v;
} else {
$new[$k] = $v;
if ($k === $after) {
$new[$key] = $value;
}
}
}
$config = $new;
}
return true;
}
/**
* Удаляет элемент из конфига
*/
public function delete(string $path): mixed
{
if (empty($this->configArray)) {
$this->configArray = $this->getArray();
}
$pathArray = \explode('=>', $path);
$size = \count($pathArray);
$i = 0;
$config = &$this->configArray;
while ($i < $size - 1) {
$key = $pathArray[$i];
if (! \array_key_exists($key, $config)) {
return false;
}
if ($this->isFormat($config[$key])) {
$config = &$config[$key]['value'];
} else {
$config = &$config[$key];
}
++$i;
}
$key = $pathArray[$i];
if (! \array_key_exists($key, $config)) {
return false;
} else {
$result = $config[$key];
unset($config[$key]);
return $result;
}
}
/**
* Записывает файл конфига с перестройкой массива
*/
public function save(): void
{
$contents = \str_replace(
$this->configStr,
$this->toStr($this->configArray, 1),
$this->fileContents,
$count
);
if (1 !== $count) {
throw new ForkException('Config array cannot be replace');
}
if (false === \file_put_contents($this->path, $contents, \LOCK_EX)) {
throw new ForkException('Config can not be write');
}
}
/**
* Преобразует массив в строку
*/
protected function toStr(array $data, int $level): string
{
$space = \str_repeat(' ', $level);
$result = '[';
$tail = '';
foreach ($data as $key => $cur) {
$tail = '';
if ($this->isFormat($cur)) {
if (\is_string($key)) {
$result .= "{$cur['key_before']}'{$key}'{$cur['key_after']}=>{$cur['value_before']}";
} else {
$result .= "{$cur['value_before']}";
}
if (\is_array($cur['value'])) {
$result .= $this->toStr($cur['value'], $level + 1) . ",{$cur['value_after']}";
} else {
$result .= "{$cur['value']},{$cur['value_after']}";
}
} else {
if (\is_string($key)) {
$result = \rtrim($result, "\n\t ");
$result .= "\n{$space}'{$key}' => ";
$tail = "\n" . \str_repeat(' ', $level - 1);
} else {
$result .= ' ';
}
if (\is_array($cur)) {
$result .= $this->toStr($cur, $level + 1) . ',';
} else {
$result .= "{$cur},";
}
}
}
return \rtrim($result . $tail, ',') . ']';
}
}