mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-17 20:54:52 +08:00
Merge pull request #146 from zhamao-robot/refactor-config
基于 LibOB Config 类重构 ZMConfig
This commit is contained in:
commit
2727b056eb
@ -7,6 +7,7 @@ use OneBot\Driver\Coroutine\CoroutineInterface;
|
||||
use OneBot\Driver\Process\ExecutionResult;
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Container\Container;
|
||||
use ZM\Container\ContainerInterface;
|
||||
use ZM\Context\Context;
|
||||
@ -141,7 +142,7 @@ function container(): ContainerInterface
|
||||
/**
|
||||
* 解析类实例(使用容器)
|
||||
*
|
||||
* @template T
|
||||
* @template T
|
||||
* @param class-string<T> $abstract
|
||||
* @return Closure|mixed|T
|
||||
* @noinspection PhpDocMissingThrowsInspection
|
||||
@ -187,3 +188,27 @@ function mysql_builder(string $name = '')
|
||||
{
|
||||
return (new MySQLWrapper($name))->createQueryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 / 设置配置项
|
||||
*
|
||||
* 传入键名和(或)默认值,获取配置项
|
||||
* 传入数组,设置配置项
|
||||
* 不传参数,返回配置容器
|
||||
*
|
||||
* @param null|array|string $key 键名
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed|void|ZMConfig
|
||||
*/
|
||||
function config($key = null, $default = null)
|
||||
{
|
||||
$config = ZMConfig::getInstance();
|
||||
if (is_null($key)) {
|
||||
return $config;
|
||||
}
|
||||
if (is_array($key)) {
|
||||
$config->set($key);
|
||||
return;
|
||||
}
|
||||
return $config->get($key, $default);
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ use ZM\Annotation\Http\Route;
|
||||
use ZM\Annotation\Interfaces\ErgodicAnnotation;
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
use ZM\Annotation\Middleware\Middleware;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Store\FileSystem;
|
||||
use ZM\Utils\HttpUtil;
|
||||
@ -92,7 +91,7 @@ class AnnotationParser
|
||||
$all_class = FileSystem::getClassesPsr4($path[0], $path[1]);
|
||||
|
||||
// 读取配置文件中配置的忽略解析的注解名,防止误解析一些别的地方需要的注解,比如@mixin
|
||||
$conf = ZMConfig::get('global.runtime.annotation_reader_ignore');
|
||||
$conf = config('global.runtime.annotation_reader_ignore');
|
||||
// 有两种方式,第一种是通过名称,第二种是通过命名空间
|
||||
if (isset($conf['name']) && is_array($conf['name'])) {
|
||||
foreach ($conf['name'] as $v) {
|
||||
|
||||
@ -7,7 +7,6 @@ namespace ZM\Command;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ZM\Config\ZMConfig;
|
||||
|
||||
class CheckConfigCommand extends Command
|
||||
{
|
||||
@ -57,7 +56,7 @@ class CheckConfigCommand extends Command
|
||||
{
|
||||
$local_file = include_once WORKING_DIR . '/config/' . $local;
|
||||
if ($local_file === true) {
|
||||
$local_file = ZMConfig::get('global');
|
||||
$local_file = config('global');
|
||||
}
|
||||
foreach ($remote as $k => $v) {
|
||||
if (!isset($local_file[$k])) {
|
||||
|
||||
@ -7,7 +7,6 @@ namespace ZM\Command\Generate;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ZM\Config\ZMConfig;
|
||||
|
||||
class SystemdGenerateCommand extends Command
|
||||
{
|
||||
@ -21,7 +20,7 @@ class SystemdGenerateCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
ZMConfig::setDirectory(SOURCE_ROOT_DIR . '/config');
|
||||
config()->addConfigPath(SOURCE_ROOT_DIR . '/config');
|
||||
$path = $this->generate();
|
||||
$output->writeln('<info>成功生成 systemd 文件,位置:' . $path . '</info>');
|
||||
$output->writeln('<info>有关如何使用 systemd 配置文件,请访问 `https://github.com/zhamao-robot/zhamao-framework/issues/36`</info>');
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Config;
|
||||
|
||||
class ConfigMetadata
|
||||
{
|
||||
public $is_patch = false;
|
||||
|
||||
public $is_env = false;
|
||||
|
||||
public $path = '';
|
||||
|
||||
public $extension = '';
|
||||
|
||||
public $data = [];
|
||||
}
|
||||
@ -4,293 +4,366 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZM\Config;
|
||||
|
||||
use OneBot\Util\Singleton;
|
||||
use OneBot\V12\Config\Config;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Store\FileSystem;
|
||||
|
||||
class ZMConfig
|
||||
class ZMConfig implements \ArrayAccess
|
||||
{
|
||||
public const SUPPORTED_EXTENSIONS = ['php', 'json'];
|
||||
|
||||
public const SUPPORTED_ENVIRONMENTS = ['development', 'production', 'staging'];
|
||||
|
||||
private const DEFAULT_PATH = __DIR__ . '/../../../config';
|
||||
|
||||
/** @var string 上次报错 */
|
||||
public static $last_error = '';
|
||||
|
||||
/** @var array 配置文件 */
|
||||
public static $config = [];
|
||||
|
||||
/** @var string 配置文件 */
|
||||
private static $path = 'config';
|
||||
|
||||
/** @var string 上次的路径 */
|
||||
private static $last_path = '.';
|
||||
|
||||
/** @var string 配置文件环境变量 */
|
||||
private static $env = 'development';
|
||||
|
||||
/** @var array 配置文件元数据 */
|
||||
private static $config_meta_list = [];
|
||||
|
||||
public static function setDirectory($path)
|
||||
{
|
||||
self::$last_path = self::$path;
|
||||
return self::$path = $path;
|
||||
}
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @var array 支持的文件扩展名
|
||||
*/
|
||||
public static function restoreDirectory()
|
||||
{
|
||||
self::$path = self::$last_path;
|
||||
self::$last_path = '.';
|
||||
}
|
||||
|
||||
public static function getDirectory(): string
|
||||
{
|
||||
return self::$path;
|
||||
}
|
||||
|
||||
public static function setEnv($env = 'development'): bool
|
||||
{
|
||||
if (!in_array($env, self::SUPPORTED_ENVIRONMENTS)) {
|
||||
throw new ConfigException('E00079', 'Unsupported environment: ' . $env);
|
||||
}
|
||||
self::$env = $env;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getEnv(): string
|
||||
{
|
||||
return self::$env;
|
||||
}
|
||||
public const ALLOWED_FILE_EXTENSIONS = ['php', 'yaml', 'yml', 'json', 'toml'];
|
||||
|
||||
/**
|
||||
* @param mixed $additional_key
|
||||
* @throws ConfigException
|
||||
* @return null|array|false|mixed
|
||||
* @var array 配置文件加载顺序,后覆盖前
|
||||
*/
|
||||
public static function get(string $name, $additional_key = '')
|
||||
{
|
||||
$separated = explode('.', $name);
|
||||
if ($additional_key !== '') {
|
||||
$separated = array_merge($separated, explode('.', $additional_key));
|
||||
}
|
||||
$head_name = array_shift($separated);
|
||||
// 首先判断有没有初始化这个配置文件,因为是只读,所以是懒加载,加载第一次后缓存起来
|
||||
if (!isset(self::$config[$head_name])) {
|
||||
logger()->debug('配置文件' . $name . ' ' . $additional_key . '没读取过,正在从文件加载 ...');
|
||||
self::$config[$head_name] = self::loadConfig($head_name);
|
||||
}
|
||||
// global.remote_terminal
|
||||
logger()->debug('根据切分来寻找子配置: ' . $name);
|
||||
$obj = self::$config[$head_name];
|
||||
foreach ($separated as $key) {
|
||||
if (isset($obj[$key])) {
|
||||
$obj = $obj[$key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function trace(string $name)
|
||||
{
|
||||
// TODO: 调试配置文件搜寻路径
|
||||
}
|
||||
|
||||
public static function reload()
|
||||
{
|
||||
self::$config = [];
|
||||
self::$config_meta_list = [];
|
||||
}
|
||||
public const LOAD_ORDER = ['default', 'environment', 'patch'];
|
||||
|
||||
/**
|
||||
* 智能patch,将patch数组内的数据合并更新到data中
|
||||
* @var string 默认配置文件路径
|
||||
*/
|
||||
public const DEFAULT_CONFIG_PATH = SOURCE_ROOT_DIR . '/config';
|
||||
|
||||
/**
|
||||
* @var array 已加载的配置文件
|
||||
*/
|
||||
private array $loaded_files = [];
|
||||
|
||||
/**
|
||||
* @var array 配置文件路径
|
||||
*/
|
||||
private array $config_paths;
|
||||
|
||||
/**
|
||||
* @var string 当前环境
|
||||
*/
|
||||
private string $environment;
|
||||
|
||||
/**
|
||||
* @var Config 内部配置容器
|
||||
*/
|
||||
private Config $holder;
|
||||
|
||||
/**
|
||||
* 构造配置实例
|
||||
*
|
||||
* @param array|mixed $data 原数据
|
||||
* @param array|mixed $patch 要patch的数据
|
||||
* @return array|mixed
|
||||
* @param array $config_paths 配置文件路径
|
||||
* @param string $environment 环境
|
||||
*
|
||||
* @throws ConfigException 配置文件加载出错
|
||||
*/
|
||||
public static function smartPatch($data, $patch)
|
||||
public function __construct(array $config_paths = [], string $environment = 'development')
|
||||
{
|
||||
/* patch 样例:
|
||||
[patch]
|
||||
runtime:
|
||||
annotation_reader_ignore: ["牛逼"]
|
||||
custom: "非常酷的patch模式"
|
||||
$this->config_paths = $config_paths ?: [self::DEFAULT_CONFIG_PATH];
|
||||
$this->environment = $environment;
|
||||
$this->holder = new Config([]);
|
||||
$this->loadFiles();
|
||||
}
|
||||
|
||||
[base]
|
||||
runtime:
|
||||
annotation_reader_ignore: []
|
||||
reload_delay_time: 800
|
||||
|
||||
[result]
|
||||
runtime:
|
||||
annotation_reader_ignore: ["牛逼"]
|
||||
reload_delay_time: 800
|
||||
custom: "非常酷的patch模式"
|
||||
*/
|
||||
if (is_array($data) && is_array($patch)) { // 两者必须是数组才行
|
||||
if (is_assoc_array($patch) && is_assoc_array($data)) { // 两者必须都是kv数组才能递归merge,如果是顺序数组,则直接覆盖
|
||||
foreach ($patch as $k => $v) {
|
||||
if (!isset($data[$k])) { // 如果项目不在基类存在,则直接写入
|
||||
$data[$k] = $v;
|
||||
} else { // 如果base存在的话,则递归patch覆盖
|
||||
$data[$k] = self::smartPatch($data[$k], $v);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
/**
|
||||
* 添加配置文件路径
|
||||
*
|
||||
* @param string $path 路径
|
||||
*/
|
||||
public function addConfigPath(string $path): void
|
||||
{
|
||||
if (!in_array($path, $this->config_paths, true)) {
|
||||
$this->config_paths[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前环境
|
||||
*
|
||||
* 变更环境后,将会自动调用 `reload` 方法重载配置
|
||||
*
|
||||
* @param string $environment 目标环境
|
||||
*/
|
||||
public function setEnvironment(string $environment): void
|
||||
{
|
||||
if ($this->environment !== $environment) {
|
||||
$this->environment = $environment;
|
||||
$this->reload();
|
||||
}
|
||||
return $patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置文件
|
||||
*
|
||||
* @throws ConfigException
|
||||
* @return array|int|string
|
||||
*/
|
||||
private static function loadConfig(string $name)
|
||||
public function loadFiles(): void
|
||||
{
|
||||
// 首先获取此名称的所有配置文件的路径
|
||||
self::parseList($name);
|
||||
$stages = [
|
||||
'default' => [],
|
||||
'environment' => [],
|
||||
'patch' => [],
|
||||
];
|
||||
|
||||
$env1_patch0 = null;
|
||||
$env1_patch1 = null;
|
||||
$env0_patch0 = null;
|
||||
$env0_patch1 = null;
|
||||
foreach (self::$config_meta_list[$name] as $v) {
|
||||
/** @var ConfigMetadata $v */
|
||||
if ($v->is_env && !$v->is_patch) {
|
||||
$env1_patch0 = $v->data;
|
||||
} elseif ($v->is_env && $v->is_patch) {
|
||||
$env1_patch1 = $v->data;
|
||||
} elseif (!$v->is_env && !$v->is_patch) {
|
||||
$env0_patch0 = $v->data;
|
||||
} else {
|
||||
$env0_patch1 = $v->data;
|
||||
}
|
||||
}
|
||||
// 优先级:无env无patch < 无env有patch < 有env无patch < 有env有patch
|
||||
// 但是无patch的版本必须有一个,否则会报错
|
||||
if ($env1_patch0 === null && $env0_patch0 === null) {
|
||||
throw new ConfigException('E00078', '未找到配置文件 ' . $name . ' !');
|
||||
}
|
||||
$data = $env1_patch0 ?? $env0_patch0;
|
||||
if (is_array($patch = $env1_patch1 ?? $env0_patch1) && is_assoc_array($patch)) {
|
||||
$data = self::smartPatch($data, $patch);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过名称将所有该名称的配置文件路径和信息读取到列表中
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
private static function parseList(string $name): void
|
||||
{
|
||||
$list = [];
|
||||
$files = FileSystem::scanDirFiles(self::$path, true, true);
|
||||
foreach ($files as $file) {
|
||||
logger()->debug('正在从目录' . self::$path . '读取配置文件 ' . $file);
|
||||
$info = pathinfo($file);
|
||||
$info['extension'] = $info['extension'] ?? '';
|
||||
|
||||
// 排除子文件夹名字带点的文件
|
||||
if ($info['dirname'] !== '.' && strpos($info['dirname'], '.') !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断文件名是否为配置文件
|
||||
if (!in_array($info['extension'], self::SUPPORTED_EXTENSIONS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ext = $info['extension'];
|
||||
$dot_separated = explode('.', $info['filename']);
|
||||
|
||||
// 将配置文件加进来
|
||||
$obj = new ConfigMetadata();
|
||||
if ($dot_separated[0] === $name) { // 如果文件名与配置文件名一致
|
||||
// 首先检测该文件是否为补丁版本儿
|
||||
if (str_ends_with($info['filename'], '.patch')) {
|
||||
$obj->is_patch = true;
|
||||
$info['filename'] = substr($info['filename'], 0, -6);
|
||||
} else {
|
||||
$obj->is_patch = false;
|
||||
}
|
||||
// 其次检测该文件是不是带有环境参数的版本儿
|
||||
if (str_ends_with($info['filename'], '.' . self::$env)) {
|
||||
$obj->is_env = true;
|
||||
$info['filename'] = substr($info['filename'], 0, -(strlen(self::$env) + 1));
|
||||
} else {
|
||||
$obj->is_env = false;
|
||||
}
|
||||
if (mb_strpos($info['filename'], '.') !== false) {
|
||||
logger()->warning('文件名 ' . $info['filename'] . ' 不合法(含有"."),请检查文件名是否合法。');
|
||||
continue;
|
||||
}
|
||||
$obj->path = zm_dir(self::$path . '/' . $info['dirname'] . '/' . $info['basename']);
|
||||
$obj->extension = $ext;
|
||||
$obj->data = self::readConfigFromFile(zm_dir(self::$path . '/' . $info['dirname'] . '/' . $info['basename']), $info['extension']);
|
||||
$list[] = $obj;
|
||||
}
|
||||
}
|
||||
// 如果是源码模式,config目录和default目录相同,所以不需要继续采摘default目录下的文件
|
||||
if (realpath(self::$path) !== realpath(self::DEFAULT_PATH)) {
|
||||
$files = FileSystem::scanDirFiles(self::DEFAULT_PATH, true, true);
|
||||
// 遍历所有需加载的文件,并按加载类型进行分组
|
||||
foreach ($this->config_paths as $config_path) {
|
||||
$files = scandir($config_path);
|
||||
foreach ($files as $file) {
|
||||
$info = pathinfo($file);
|
||||
$info['extension'] = $info['extension'] ?? '';
|
||||
// 判断文件名是否为配置文件
|
||||
if (!in_array($info['extension'], self::SUPPORTED_EXTENSIONS)) {
|
||||
[, $ext, $load_type,] = $this->getFileMeta($file);
|
||||
// 略过不支持的文件
|
||||
if (!in_array($ext, self::ALLOWED_FILE_EXTENSIONS, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($info['filename'] === $name) { // 如果文件名与配置文件名一致,就创建一个配置文件的元数据对象
|
||||
$obj = new ConfigMetadata();
|
||||
$obj->is_patch = false;
|
||||
$obj->is_env = false;
|
||||
$obj->path = realpath(self::DEFAULT_PATH . '/' . $info['dirname'] . '/' . $info['basename']);
|
||||
$obj->extension = $info['extension'];
|
||||
$obj->data = self::readConfigFromFile(zm_dir(self::DEFAULT_PATH . '/' . $info['dirname'] . '/' . $info['basename']), $info['extension']);
|
||||
$list[] = $obj;
|
||||
|
||||
$file_path = zm_dir($config_path . '/' . $file);
|
||||
if (is_dir($file_path)) {
|
||||
// TODO: 支持子目录(待定)
|
||||
continue;
|
||||
}
|
||||
|
||||
// 略过不应加载的文件
|
||||
if (!$this->shouldLoadFile($file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 略过加载顺序未知的文件
|
||||
if (!in_array($load_type, self::LOAD_ORDER, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 将文件加入到对应的加载阶段
|
||||
$stages[$load_type][] = $file_path;
|
||||
}
|
||||
}
|
||||
|
||||
// 按照加载顺序加载配置文件
|
||||
foreach (self::LOAD_ORDER as $load_type) {
|
||||
foreach ($stages[$load_type] as $file_path) {
|
||||
logger()->info("加载配置文件:{$file_path}");
|
||||
$this->loadConfigFromPath($file_path);
|
||||
}
|
||||
}
|
||||
self::$config_meta_list[$name] = $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据不同的扩展类型读取配置文件数组
|
||||
* 合并传入的配置数组至指定的配置项
|
||||
*
|
||||
* 请注意内部实现是 array_replace_recursive,而不是 array_merge
|
||||
*
|
||||
* @param string $key 目标配置项,必须为数组
|
||||
* @param array $config 要合并的配置数组
|
||||
*/
|
||||
public function merge(string $key, array $config): void
|
||||
{
|
||||
$original = $this->get($key, []);
|
||||
$this->set($key, array_replace_recursive($original, $config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项
|
||||
*
|
||||
* @param string $key 配置项名称,可使用.访问数组
|
||||
* @param mixed $default 默认值
|
||||
*
|
||||
* @return null|array|mixed
|
||||
*/
|
||||
public function get(string $key, $default = null)
|
||||
{
|
||||
return $this->holder->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置项
|
||||
* 仅在本次运行期间生效,不会保存到配置文件中哦
|
||||
*
|
||||
* 如果传入的是数组,则会将键名作为配置项名称,并将值作为配置项的值
|
||||
* 顺带一提,数组支持批量设置
|
||||
*
|
||||
* @param array|string $key 配置项名称,可使用.访问数组
|
||||
* @param mixed $value 要写入的值,传入 null 会进行删除
|
||||
*/
|
||||
public function set($key, $value = null): void
|
||||
{
|
||||
$keys = is_array($key) ? $key : [$key => $value];
|
||||
foreach ($keys as $i_key => $i_val) {
|
||||
$this->holder->set($i_key, $i_val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内部配置容器
|
||||
*/
|
||||
public function getHolder(): Config
|
||||
{
|
||||
return $this->holder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载配置文件
|
||||
* 运行期间新增的配置文件不会被加载哟~
|
||||
*
|
||||
* @param mixed|string $filename 文件名
|
||||
* @param mixed|string $ext_name 扩展名
|
||||
* @throws ConfigException
|
||||
*/
|
||||
private static function readConfigFromFile($filename, $ext_name): array
|
||||
public function reload(): void
|
||||
{
|
||||
logger()->debug('正加载配置文件 ' . $filename);
|
||||
switch ($ext_name) {
|
||||
case 'php':
|
||||
$r = require $filename;
|
||||
if (is_array($r)) {
|
||||
return $r;
|
||||
}
|
||||
throw new ConfigException('E00079', 'php配置文件include失败,请检查终端warning错误');
|
||||
case 'json':
|
||||
default:
|
||||
$r = json_decode(file_get_contents($filename), true);
|
||||
if (is_array($r)) {
|
||||
return $r;
|
||||
}
|
||||
throw new ConfigException('E00079', 'json反序列化失败,请检查文件内容');
|
||||
$this->holder = new Config([]);
|
||||
$this->loadFiles();
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return $this->get($offset) !== null;
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->get($offset);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->set($offset, $value);
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
$this->set($offset, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件元信息
|
||||
*
|
||||
* @param string $name 文件名
|
||||
*
|
||||
* @return array 文件元信息,数组元素按次序为:配置组名/扩展名/加载类型/环境类型
|
||||
*/
|
||||
private function getFileMeta(string $name): array
|
||||
{
|
||||
$basename = pathinfo($name, PATHINFO_BASENAME);
|
||||
$parts = explode('.', $basename);
|
||||
$ext = array_pop($parts);
|
||||
$load_type = $this->getFileLoadType(implode('.', $parts));
|
||||
if ($load_type === 'default') {
|
||||
$env = null;
|
||||
} else {
|
||||
$env = array_pop($parts);
|
||||
}
|
||||
$group = implode('.', $parts);
|
||||
return [$group, $ext, $load_type, $env];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件加载类型
|
||||
*
|
||||
* @param string $name 文件名,不带扩展名
|
||||
*
|
||||
* @return string 可能为:default, environment, patch
|
||||
*/
|
||||
private function getFileLoadType(string $name): string
|
||||
{
|
||||
// 传入此处的 name 参数有三种可能的格式:
|
||||
// 1. 纯文件名:如 test,此时加载类型为 default
|
||||
// 2. 文件名.环境:如 test.development,此时加载类型为 environment
|
||||
// 3. 文件名.patch:如 test.patch,此时加载类型为 patch
|
||||
// 至于其他的格式,则为未定义行为
|
||||
if (strpos($name, '.') === false) {
|
||||
return 'default';
|
||||
}
|
||||
$name_and_env = explode('.', $name);
|
||||
if (count($name_and_env) !== 2) {
|
||||
return 'undefined';
|
||||
}
|
||||
if ($name_and_env[1] === 'patch') {
|
||||
return 'patch';
|
||||
}
|
||||
return 'environment';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该加载配置文件
|
||||
*
|
||||
* @param string $path 文件名,包含扩展名
|
||||
*/
|
||||
private function shouldLoadFile(string $path): bool
|
||||
{
|
||||
$name = pathinfo($path, PATHINFO_FILENAME);
|
||||
// 对于 `default` 和 `patch`,任何情况下均应加载
|
||||
// 对于 `environment`,只有当环境与当前环境相同时才加载
|
||||
// 对于其他情况,则不加载
|
||||
$type = $this->getFileLoadType($name);
|
||||
if ($type === 'default' || $type === 'patch') {
|
||||
return true;
|
||||
}
|
||||
if ($type === 'environment') {
|
||||
$name_and_env = explode('.', $name);
|
||||
if ($name_and_env[1] === $this->environment) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从传入的路径加载配置文件
|
||||
*
|
||||
* @param string $path 配置文件路径
|
||||
*
|
||||
* @throws ConfigException 传入的配置文件不支持
|
||||
*/
|
||||
private function loadConfigFromPath(string $path): void
|
||||
{
|
||||
if (in_array($path, $this->loaded_files, true)) {
|
||||
return;
|
||||
}
|
||||
$this->loaded_files[] = $path;
|
||||
|
||||
// 判断文件格式是否支持
|
||||
[$group, $ext, $load_type, $env] = $this->getFileMeta($path);
|
||||
if (!in_array($ext, self::ALLOWED_FILE_EXTENSIONS, true)) {
|
||||
throw ConfigException::unsupportedFileType($path);
|
||||
}
|
||||
|
||||
// 读取并解析配置
|
||||
$content = file_get_contents($path);
|
||||
$config = [];
|
||||
switch ($ext) {
|
||||
case 'php':
|
||||
$config = require $path;
|
||||
break;
|
||||
case 'json':
|
||||
try {
|
||||
$config = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw ConfigException::loadConfigFailed($path, $e->getMessage());
|
||||
}
|
||||
break;
|
||||
case 'yaml':
|
||||
case 'yml':
|
||||
$yaml_parser_class = 'Symfony\Component\Yaml\Yaml';
|
||||
if (!class_exists($yaml_parser_class)) {
|
||||
throw ConfigException::loadConfigFailed($path, 'YAML 解析器未安装');
|
||||
}
|
||||
try {
|
||||
$config = $yaml_parser_class::parse($content);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw ConfigException::loadConfigFailed($path, $e->getMessage());
|
||||
}
|
||||
break;
|
||||
case 'toml':
|
||||
$toml_parser_class = 'Yosymfony\Toml\Toml';
|
||||
if (!class_exists($toml_parser_class)) {
|
||||
throw ConfigException::loadConfigFailed($path, 'TOML 解析器未安装');
|
||||
}
|
||||
try {
|
||||
$config = $toml_parser_class::parse($content);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw ConfigException::loadConfigFailed($path, $e->getMessage());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw ConfigException::unsupportedFileType($path);
|
||||
}
|
||||
|
||||
// 加入配置
|
||||
$this->merge($group, $config);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,10 +10,8 @@ use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||||
use OneBot\Driver\Process\ProcessManager;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Context\Context;
|
||||
use ZM\Context\ContextInterface;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Framework;
|
||||
|
||||
class ContainerServicesProvider
|
||||
@ -29,8 +27,7 @@ class ContainerServicesProvider
|
||||
* connection: open, close, message
|
||||
* ```
|
||||
*
|
||||
* @param string $scope 作用域
|
||||
* @throws ConfigException
|
||||
* @param string $scope 作用域
|
||||
*/
|
||||
public function registerServices(string $scope, ...$params): void
|
||||
{
|
||||
@ -63,8 +60,6 @@ class ContainerServicesProvider
|
||||
|
||||
/**
|
||||
* 注册全局服务
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
private function registerGlobalServices(ContainerInterface $container): void
|
||||
{
|
||||
@ -72,7 +67,7 @@ class ContainerServicesProvider
|
||||
$container->instance('path.working', WORKING_DIR);
|
||||
$container->instance('path.source', SOURCE_ROOT_DIR);
|
||||
$container->alias('path.source', 'path.base');
|
||||
$container->instance('path.data', ZMConfig::get('global.data_dir'));
|
||||
$container->instance('path.data', config('global.data_dir'));
|
||||
$container->instance('path.framework', FRAMEWORK_ROOT_DIR);
|
||||
|
||||
// 注册worker和驱动
|
||||
|
||||
@ -7,7 +7,6 @@ namespace ZM\Event\Listener;
|
||||
use OneBot\Driver\Workerman\Worker;
|
||||
use OneBot\Util\Singleton;
|
||||
use Swoole\Server;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Exception\ZMKnownException;
|
||||
use ZM\Framework;
|
||||
use ZM\Process\ProcessStateManager;
|
||||
@ -27,7 +26,7 @@ class MasterEventListener
|
||||
SignalListener::getInstance()->signalMaster();
|
||||
}
|
||||
ProcessStateManager::saveProcessState(ONEBOT_PROCESS_MASTER, $server->master_pid, [
|
||||
'stdout' => ZMConfig::get('global.swoole_options.swoole_set.log_file'),
|
||||
'stdout' => config('global.swoole_options.swoole_set.log_file'),
|
||||
'daemon' => (bool) Framework::getInstance()->getArgv()['daemon'],
|
||||
]);
|
||||
});
|
||||
|
||||
@ -11,7 +11,6 @@ use ZM\Annotation\AnnotationHandler;
|
||||
use ZM\Annotation\AnnotationMap;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Framework\Init;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Container\ContainerServicesProvider;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Exception\ZMKnownException;
|
||||
@ -161,7 +160,7 @@ class WorkerEventListener
|
||||
}
|
||||
|
||||
// 读取 MySQL 配置文件
|
||||
$conf = ZMConfig::get('global.mysql');
|
||||
$conf = config('global.mysql');
|
||||
if (is_array($conf) && !is_assoc_array($conf)) {
|
||||
// 如果有多个数据库连接,则遍历
|
||||
foreach ($conf as $conn_conf) {
|
||||
|
||||
@ -8,8 +8,22 @@ use Throwable;
|
||||
|
||||
class ConfigException extends ZMException
|
||||
{
|
||||
public const UNSUPPORTED_FILE_TYPE = 'E00079';
|
||||
|
||||
public const LOAD_CONFIG_FAILED = 'E00080';
|
||||
|
||||
public function __construct($err_code, $message = '', $code = 0, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(zm_internal_errcode($err_code) . $message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function unsupportedFileType(string $file_path): ConfigException
|
||||
{
|
||||
return new self(self::UNSUPPORTED_FILE_TYPE, "不支持的配置文件类型:{$file_path}");
|
||||
}
|
||||
|
||||
public static function loadConfigFailed(string $file_path, string $message): ConfigException
|
||||
{
|
||||
return new self(self::LOAD_CONFIG_FAILED, "加载配置文件失败:{$file_path},{$message}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ use OneBot\Driver\Workerman\WorkermanDriver;
|
||||
use OneBot\Util\Singleton;
|
||||
use Phar;
|
||||
use ZM\Command\Server\ServerStartCommand;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Event\EventProvider;
|
||||
use ZM\Event\Listener\HttpEventListener;
|
||||
use ZM\Event\Listener\ManagerEventListener;
|
||||
@ -189,8 +188,8 @@ class Framework
|
||||
}
|
||||
foreach ($find_dir as $v) {
|
||||
if (is_dir($v)) {
|
||||
ZMConfig::setDirectory($v);
|
||||
ZMConfig::setEnv($this->argv['env'] = $this->argv['env'] ?? 'development');
|
||||
config()->addConfigPath($v);
|
||||
config()->setEnvironment($this->argv['env'] = ($this->argv['env'] ?? 'development'));
|
||||
$config_done = true;
|
||||
break;
|
||||
}
|
||||
@ -214,7 +213,7 @@ class Framework
|
||||
$ob_event_provider = EventProvider::getInstance();
|
||||
|
||||
// 初始化时区,默认为上海时区
|
||||
date_default_timezone_set(ZMConfig::get('global.runtime.timezone'));
|
||||
date_default_timezone_set(config('global.runtime.timezone'));
|
||||
|
||||
// 注册全局错误处理器
|
||||
set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) {
|
||||
@ -251,20 +250,20 @@ class Framework
|
||||
*/
|
||||
public function initDriver()
|
||||
{
|
||||
switch ($driver = ZMConfig::get('global.driver')) {
|
||||
switch ($driver = config('global.driver')) {
|
||||
case 'swoole':
|
||||
if (DIRECTORY_SEPARATOR === '\\') {
|
||||
logger()->emergency('Windows does not support swoole driver!');
|
||||
exit(1);
|
||||
}
|
||||
ZMConfig::$config['global']['swoole_options']['driver_init_policy'] = DriverInitPolicy::MULTI_PROCESS_INIT_IN_MASTER;
|
||||
$this->driver = new SwooleDriver(ZMConfig::get('global.swoole_options'));
|
||||
$this->driver->initDriverProtocols(ZMConfig::get('global.servers'));
|
||||
config(['global.swoole_options.driver_init_policy' => DriverInitPolicy::MULTI_PROCESS_INIT_IN_MASTER]);
|
||||
$this->driver = new SwooleDriver(config('global.swoole_options'));
|
||||
$this->driver->initDriverProtocols(config('global.servers'));
|
||||
break;
|
||||
case 'workerman':
|
||||
ZMConfig::$config['global']['workerman_options']['driver_init_policy'] = DriverInitPolicy::MULTI_PROCESS_INIT_IN_MASTER;
|
||||
$this->driver = new WorkermanDriver(ZMConfig::get('global.workerman_options'));
|
||||
$this->driver->initDriverProtocols(ZMConfig::get('global.servers'));
|
||||
config(['global.workerman_options.driver_init_policy' => DriverInitPolicy::MULTI_PROCESS_INIT_IN_MASTER]);
|
||||
$this->driver = new WorkermanDriver(config('global.workerman_options'));
|
||||
$this->driver->initDriverProtocols(config('global.servers'));
|
||||
break;
|
||||
default:
|
||||
logger()->error(zm_internal_errcode('E00081') . '未知的驱动类型 ' . $driver . ' !');
|
||||
@ -327,9 +326,9 @@ class Framework
|
||||
// 打印环境信息
|
||||
$properties['environment'] = $this->argv['env'];
|
||||
// 打印驱动
|
||||
$properties['driver'] = ZMConfig::get('global.driver');
|
||||
$properties['driver'] = config('global.driver');
|
||||
// 打印logger显示等级
|
||||
$properties['log_level'] = $this->argv['log-level'] ?? ZMConfig::get('global', 'log_level') ?? 'info';
|
||||
$properties['log_level'] = $this->argv['log-level'] ?? config('global.log_level') ?? 'info';
|
||||
// 打印框架版本
|
||||
$properties['version'] = self::VERSION . (LOAD_MODE === 0 ? (' (build ' . ZM_VERSION_ID . ')') : '');
|
||||
// 打印 PHP 版本
|
||||
@ -342,8 +341,8 @@ class Framework
|
||||
if ($this->driver->getName() === 'swoole') {
|
||||
$properties['process_mode'] = 'MST1';
|
||||
ProcessStateManager::$process_mode['master'] = 1;
|
||||
if (ZMConfig::get('global.swoole_options.swoole_server_mode') === SWOOLE_BASE) {
|
||||
$worker_num = ZMConfig::get('global.swoole_options.swoole_set.worker_num');
|
||||
if (config('global.swoole_options.swoole_server_mode') === SWOOLE_BASE) {
|
||||
$worker_num = config('global.swoole_options.swoole_set.worker_num');
|
||||
if ($worker_num === null || $worker_num === 1) {
|
||||
$properties['process_mode'] .= 'MAN0#0';
|
||||
ProcessStateManager::$process_mode['manager'] = 0;
|
||||
@ -353,12 +352,12 @@ class Framework
|
||||
ProcessStateManager::$process_mode['manager'] = 0;
|
||||
ProcessStateManager::$process_mode['worker'] = swoole_cpu_num();
|
||||
} else {
|
||||
$properties['process_mode'] .= 'MAN0#' . ($worker = ZMConfig::get('global.swoole_options.swoole_set.worker_num') ?? swoole_cpu_num());
|
||||
$properties['process_mode'] .= 'MAN0#' . ($worker = config('global.swoole_options.swoole_set.worker_num') ?? swoole_cpu_num());
|
||||
ProcessStateManager::$process_mode['manager'] = 0;
|
||||
ProcessStateManager::$process_mode['worker'] = $worker;
|
||||
}
|
||||
} else {
|
||||
$worker = ZMConfig::get('global.swoole_options.swoole_set.worker_num') === 0 ? swoole_cpu_num() : ZMConfig::get('global.swoole_options.swoole_set.worker_num') ?? swoole_cpu_num();
|
||||
$worker = config('global.swoole_options.swoole_set.worker_num') === 0 ? swoole_cpu_num() : config('global.swoole_options.swoole_set.worker_num') ?? swoole_cpu_num();
|
||||
$properties['process_mode'] .= 'MAN1#' . $worker;
|
||||
ProcessStateManager::$process_mode['manager'] = 1;
|
||||
ProcessStateManager::$process_mode['worker'] = $worker;
|
||||
@ -366,7 +365,7 @@ class Framework
|
||||
} elseif ($this->driver->getName() === 'workerman') {
|
||||
$properties['process_mode'] = 'MST1';
|
||||
ProcessStateManager::$process_mode['master'] = 1;
|
||||
$worker_num = ZMConfig::get('global.workerman_options.workerman_worker_num');
|
||||
$worker_num = config('global.workerman_options.workerman_worker_num');
|
||||
if (DIRECTORY_SEPARATOR === '\\') {
|
||||
$properties['process_mode'] .= '#0';
|
||||
ProcessStateManager::$process_mode['manager'] = 0;
|
||||
@ -379,17 +378,17 @@ class Framework
|
||||
}
|
||||
}
|
||||
// 打印监听端口
|
||||
foreach (ZMConfig::get('global.servers') as $k => $v) {
|
||||
foreach (config('global.servers') as $k => $v) {
|
||||
$properties['listen_' . $k] = $v['type'] . '://' . $v['host'] . ':' . $v['port'];
|
||||
}
|
||||
// 打印 MySQL 连接信息
|
||||
if ((ZMConfig::get('global.mysql_config.host') ?? '') !== '') {
|
||||
$conf = ZMConfig::get('global', 'mysql_config');
|
||||
if ((config('global.mysql_config.host') ?? '') !== '') {
|
||||
$conf = config('global', 'mysql_config');
|
||||
$properties['mysql'] = $conf['dbname'] . '@' . $conf['host'] . ':' . $conf['port'];
|
||||
}
|
||||
// 打印 Redis 连接信息
|
||||
if ((ZMConfig::get('global', 'redis_config')['host'] ?? '') !== '') {
|
||||
$conf = ZMConfig::get('global', 'redis_config');
|
||||
if ((config('global', 'redis_config')['host'] ?? '') !== '') {
|
||||
$conf = config('global', 'redis_config');
|
||||
$properties['redis_pool'] = $conf['host'] . ':' . $conf['port'];
|
||||
}
|
||||
|
||||
@ -480,14 +479,14 @@ class Framework
|
||||
}
|
||||
switch ($x) {
|
||||
case 'driver': // 动态设置驱动类型
|
||||
ZMConfig::$config['global']['driver'] = $y;
|
||||
config()['global']['driver'] = $y;
|
||||
break;
|
||||
case 'worker-num': // 动态设置 Worker 数量
|
||||
ZMConfig::$config['global']['swoole_options']['swoole_set']['worker_num'] = intval($y);
|
||||
ZMConfig::$config['global']['workerman_options']['workerman_worker_num'] = intval($y);
|
||||
config()['global']['swoole_options']['swoole_set']['worker_num'] = intval($y);
|
||||
config()['global']['workerman_options']['workerman_worker_num'] = intval($y);
|
||||
break;
|
||||
case 'daemon': // 启动为守护进程
|
||||
ZMConfig::$config['global']['swoole_options']['swoole_set']['daemonize'] = 1;
|
||||
config()['global']['swoole_options']['swoole_set']['daemonize'] = 1;
|
||||
Worker::$daemonize = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ namespace ZM;
|
||||
|
||||
use Exception;
|
||||
use ZM\Command\Server\ServerStartCommand;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Exception\InitException;
|
||||
use ZM\Plugin\InstantPlugin;
|
||||
|
||||
@ -40,7 +39,7 @@ class InstantApplication extends InstantPlugin
|
||||
|
||||
public function withArgs(array $args): InstantApplication
|
||||
{
|
||||
$this->args = ZMConfig::smartPatch($this->args, $args);
|
||||
$this->args = array_replace_recursive($this->args, $args);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ namespace ZM\Store\MySQL;
|
||||
use Doctrine\DBAL\Driver as DoctrineDriver;
|
||||
use Doctrine\DBAL\Platforms\MySqlPlatform;
|
||||
use Doctrine\DBAL\Schema\MySqlSchemaManager;
|
||||
use ZM\Config\ZMConfig;
|
||||
|
||||
class MySQLDriver implements DoctrineDriver
|
||||
{
|
||||
@ -34,7 +33,7 @@ class MySQLDriver implements DoctrineDriver
|
||||
|
||||
public function getDatabase($conn)
|
||||
{
|
||||
$conf = ZMConfig::get('global.mysql');
|
||||
$conf = config('global.mysql');
|
||||
|
||||
if ($conn instanceof MySQLConnection) {
|
||||
foreach ($conf as $v) {
|
||||
|
||||
@ -13,7 +13,6 @@ use Symfony\Component\Routing\Exception\ResourceNotFoundException;
|
||||
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Store\FileSystem;
|
||||
|
||||
@ -80,13 +79,13 @@ class HttpUtil
|
||||
public static function handleStaticPage(string $uri, array $settings = []): ResponseInterface
|
||||
{
|
||||
// 确定根目录
|
||||
$base_dir = $settings['document_root'] ?? ZMConfig::get('global.file_server.document_root');
|
||||
$base_dir = $settings['document_root'] ?? config('global.file_server.document_root');
|
||||
// 将相对路径转换为绝对路径
|
||||
if (FileSystem::isRelativePath($base_dir)) {
|
||||
$base_dir = SOURCE_ROOT_DIR . '/' . $base_dir;
|
||||
}
|
||||
// 支持默认缺省搜索的文件名(如index.html)
|
||||
$base_index = $settings['document_index'] ?? ZMConfig::get('global.file_server.document_index');
|
||||
$base_index = $settings['document_index'] ?? config('global.file_server.document_index');
|
||||
if (is_string($base_index)) {
|
||||
$base_index = [$base_index];
|
||||
}
|
||||
@ -110,7 +109,7 @@ class HttpUtil
|
||||
logger()->info('[200] ' . $uri);
|
||||
$exp = strtolower(pathinfo($path . $vp)['extension'] ?? 'unknown');
|
||||
return HttpFactory::getInstance()->createResponse()
|
||||
->withAddedHeader('Content-Type', ZMConfig::get('file_header')[$exp] ?? 'application/octet-stream')
|
||||
->withAddedHeader('Content-Type', config('file_header')[$exp] ?? 'application/octet-stream')
|
||||
->withBody(HttpFactory::getInstance()->createStream(file_get_contents($path . '/' . $vp)));
|
||||
}
|
||||
}
|
||||
@ -119,7 +118,7 @@ class HttpUtil
|
||||
logger()->info('[200] ' . $uri);
|
||||
$exp = strtolower(pathinfo($path)['extension'] ?? 'unknown');
|
||||
return HttpFactory::getInstance()->createResponse()
|
||||
->withAddedHeader('Content-Type', ZMConfig::get('file_header')[$exp] ?? 'application/octet-stream')
|
||||
->withAddedHeader('Content-Type', config('file_header')[$exp] ?? 'application/octet-stream')
|
||||
->withBody(HttpFactory::getInstance()->createStream(file_get_contents($path)));
|
||||
}
|
||||
}
|
||||
@ -136,14 +135,14 @@ class HttpUtil
|
||||
public static function handleHttpCodePage(int $code): ResponseInterface
|
||||
{
|
||||
// 获取有没有规定 code page
|
||||
$code_page = ZMConfig::get('global.file_server.document_code_page')[$code] ?? null;
|
||||
if ($code_page !== null && !file_exists((ZMConfig::get('global.file_server.document_root') ?? '/not/exist/') . '/' . $code_page)) {
|
||||
$code_page = config('global.file_server.document_code_page')[$code] ?? null;
|
||||
if ($code_page !== null && !file_exists((config('global.file_server.document_root') ?? '/not/exist/') . '/' . $code_page)) {
|
||||
$code_page = null;
|
||||
}
|
||||
if ($code_page === null) {
|
||||
return HttpFactory::getInstance()->createResponse($code);
|
||||
}
|
||||
return HttpFactory::getInstance()->createResponse($code, null, [], file_get_contents(ZMConfig::get('global.file_server.document_root') . '/' . $code_page));
|
||||
return HttpFactory::getInstance()->createResponse($code, null, [], file_get_contents(config('global.file_server.document_root') . '/' . $code_page));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -120,4 +120,21 @@ class ReflectionUtil
|
||||
? new ReflectionMethod($callback[0], $callback[1])
|
||||
: new ReflectionFunction($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传入的类方法,并确保其可访问
|
||||
*
|
||||
* 请不要滥用此方法!!!
|
||||
*
|
||||
* @param string $class 类名
|
||||
* @param string $method 方法名
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public static function getMethod(string $class, string $method): ReflectionMethod
|
||||
{
|
||||
$class = new \ReflectionClass($class);
|
||||
$method = $class->getMethod($method);
|
||||
$method->setAccessible(true);
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
||||
171
tests/ZM/Config/ZMConfigTest.php
Normal file
171
tests/ZM/Config/ZMConfigTest.php
Normal file
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\ZM\Config;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Utils\ReflectionUtil;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ZMConfigTest extends TestCase
|
||||
{
|
||||
private static ZMConfig $config;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
$mock_dir = __DIR__ . '/config_mock';
|
||||
if (!is_dir($mock_dir)) {
|
||||
mkdir($mock_dir, 0755, true);
|
||||
}
|
||||
|
||||
$test_config = [
|
||||
'foo' => 'bar',
|
||||
'bar' => 'baz',
|
||||
'baz' => 'bat',
|
||||
'null' => null,
|
||||
'boolean' => true,
|
||||
'associate' => [
|
||||
'x' => 'xxx',
|
||||
'y' => 'yyy',
|
||||
],
|
||||
'array' => [
|
||||
'aaa',
|
||||
'zzz',
|
||||
],
|
||||
'x' => [
|
||||
'z' => 'zoo',
|
||||
],
|
||||
'a.b' => 'c',
|
||||
'a' => [
|
||||
'b.c' => 'd',
|
||||
],
|
||||
'default' => 'yes',
|
||||
'another array' => [
|
||||
'foo', 'bar',
|
||||
],
|
||||
];
|
||||
|
||||
// 下方测试需要临时写入的文件
|
||||
file_put_contents($mock_dir . '/test.php', '<?php return ' . var_export($test_config, true) . ';');
|
||||
file_put_contents(
|
||||
$mock_dir . '/test.development.php',
|
||||
'<?php return ["environment" => "yes", "env" => "development"];'
|
||||
);
|
||||
file_put_contents(
|
||||
$mock_dir . '/test.production.php',
|
||||
'<?php return ["environment" => "yes", "env" => "production"];'
|
||||
);
|
||||
file_put_contents(
|
||||
$mock_dir . '/test.patch.php',
|
||||
'<?php return ["patch" => "yes", "another array" => ["far", "baz"]];'
|
||||
);
|
||||
|
||||
$config = new ZMConfig([
|
||||
__DIR__ . '/config_mock',
|
||||
], 'development');
|
||||
self::$config = $config;
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
foreach (scandir(__DIR__ . '/config_mock') as $file) {
|
||||
if ($file !== '.' && $file !== '..') {
|
||||
unlink(__DIR__ . '/config_mock/' . $file);
|
||||
}
|
||||
}
|
||||
rmdir(__DIR__ . '/config_mock');
|
||||
}
|
||||
|
||||
public function testGetValueWhenKeyContainsDot(): void
|
||||
{
|
||||
$this->markTestSkipped('should it be supported?');
|
||||
$this->assertEquals('c', self::$config->get('test.a.b'));
|
||||
$this->assertEquals('d', self::$config->get('test.a.b.c'));
|
||||
}
|
||||
|
||||
public function testGetBooleanValue(): void
|
||||
{
|
||||
$this->assertTrue(self::$config->get('test.boolean'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerTestGetValue
|
||||
* @param mixed $expected
|
||||
*/
|
||||
public function testGetValue(string $key, $expected): void
|
||||
{
|
||||
$this->assertSame($expected, self::$config->get($key));
|
||||
}
|
||||
|
||||
public function providerTestGetValue(): array
|
||||
{
|
||||
return [
|
||||
'null' => ['test.null', null],
|
||||
'boolean' => ['test.boolean', true],
|
||||
'associate' => ['test.associate', ['x' => 'xxx', 'y' => 'yyy']],
|
||||
'array' => ['test.array', ['aaa', 'zzz']],
|
||||
'dot access' => ['test.x.z', 'zoo'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testGetWithDefault(): void
|
||||
{
|
||||
$this->assertSame('default', self::$config->get('not_exist', 'default'));
|
||||
}
|
||||
|
||||
public function testSetValue(): void
|
||||
{
|
||||
self::$config->set('key', 'value');
|
||||
$this->assertSame('value', self::$config->get('key'));
|
||||
}
|
||||
|
||||
public function testSetArrayValue(): void
|
||||
{
|
||||
self::$config->set('array', ['a', 'b']);
|
||||
$this->assertSame(['a', 'b'], self::$config->get('array'));
|
||||
$this->assertSame('a', self::$config->get('array.0'));
|
||||
}
|
||||
|
||||
public function testGetEnvironmentSpecifiedValue(): void
|
||||
{
|
||||
$this->assertSame('yes', self::$config->get('test.environment'));
|
||||
$this->assertSame('development', self::$config->get('test.env'));
|
||||
}
|
||||
|
||||
public function testGetPatchSpecifiedValue(): void
|
||||
{
|
||||
$this->assertSame('yes', self::$config->get('test.patch'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerTestGetFileLoadType
|
||||
*/
|
||||
public function testGetFileLoadType(string $name, string $type): void
|
||||
{
|
||||
$method = ReflectionUtil::getMethod(ZMConfig::class, 'getFileLoadType');
|
||||
$actual = $method->invokeArgs(self::$config, [$name]);
|
||||
$this->assertSame($type, $actual);
|
||||
}
|
||||
|
||||
public function providerTestGetFileLoadType(): array
|
||||
{
|
||||
return [
|
||||
'default' => ['test', 'default'],
|
||||
'environment' => ['test.development', 'environment'],
|
||||
'patch' => ['test.patch', 'patch'],
|
||||
// complex case are not supported yet
|
||||
'invalid' => ['test.patch.development', 'undefined'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testArrayReplaceInsteadOfMerge(): void
|
||||
{
|
||||
// using of space inside config key is not an officially supported feature,
|
||||
// it may be removed in the future, please avoid using it in your project.
|
||||
$this->assertSame(['far', 'baz'], self::$config->get('test.another array'));
|
||||
}
|
||||
}
|
||||
@ -1,164 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\ZM\Config;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Utils\DataProvider;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ZMConfigTest extends TestCase
|
||||
{
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
$mock_dir = __DIR__ . '/config_mock';
|
||||
ZMConfig::reload();
|
||||
ZMConfig::setDirectory(__DIR__ . '/config_mock');
|
||||
if (!is_dir($mock_dir)) {
|
||||
mkdir($mock_dir, 0755, true);
|
||||
}
|
||||
// 下方测试需要临时写入的文件
|
||||
file_put_contents($mock_dir . '/global.patch.php', '<?php return ["port" => 30055];');
|
||||
file_put_contents($mock_dir . '/php_exception.php', '<?php return true;');
|
||||
file_put_contents($mock_dir . '/json_exception.json', '"string"');
|
||||
file_put_contents($mock_dir . '/global.development.patch.php', '<?php return ["port" => 30055];');
|
||||
file_put_contents($mock_dir . '/global.invalid.development.php', '<?php return ["port" => 30055];');
|
||||
file_put_contents($mock_dir . '/fake.development.json', '{"multi":{"level":"test"}}');
|
||||
file_put_contents($mock_dir . '/no_main_only_patch.patch.json', '{"multi":{"level":"test"}}');
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
ZMConfig::reload();
|
||||
ZMConfig::restoreDirectory();
|
||||
foreach (DataProvider::scanDirFiles(__DIR__ . '/config_mock', true, false) as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
rmdir(__DIR__ . '/config_mock');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public function testReload()
|
||||
{
|
||||
$this->markTestIncomplete('logger level change in need');
|
||||
$this->expectOutputRegex('/没读取过,正在从文件加载/');
|
||||
$this->assertEquals('0.0.0.0', ZMConfig::get('global.host'));
|
||||
ZMConfig::reload();
|
||||
Console::setLevel(4);
|
||||
$this->assertEquals('0.0.0.0', ZMConfig::get('global.host'));
|
||||
Console::setLevel(0);
|
||||
}
|
||||
|
||||
public function testSetAndRestoreDirectory()
|
||||
{
|
||||
$origin = ZMConfig::getDirectory();
|
||||
ZMConfig::setDirectory('.');
|
||||
$this->assertEquals('.', ZMConfig::getDirectory());
|
||||
ZMConfig::restoreDirectory();
|
||||
$this->assertEquals($origin, ZMConfig::getDirectory());
|
||||
}
|
||||
|
||||
public function testSetAndGetEnv()
|
||||
{
|
||||
$this->expectException(ConfigException::class);
|
||||
ZMConfig::setEnv('production');
|
||||
$this->assertEquals('production', ZMConfig::getEnv());
|
||||
ZMConfig::setEnv();
|
||||
ZMConfig::setEnv('reee');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerTestGet
|
||||
* @param mixed $expected
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public function testGet(array $data_params, $expected)
|
||||
{
|
||||
$this->assertEquals($expected, ZMConfig::get(...$data_params));
|
||||
}
|
||||
|
||||
public function providerTestGet(): array
|
||||
{
|
||||
return [
|
||||
'get port' => [['global.port'], 30055],
|
||||
'get port key 2' => [['global', 'port'], 30055],
|
||||
'get invalid key' => [['global', 'invalid'], null],
|
||||
'get another environment' => [['fake.multi.level'], 'test'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testGetPhpException()
|
||||
{
|
||||
$this->expectException(ConfigException::class);
|
||||
ZMConfig::get('php_exception');
|
||||
}
|
||||
|
||||
public function testGetJsonException()
|
||||
{
|
||||
$this->expectException(ConfigException::class);
|
||||
ZMConfig::get('json_exception');
|
||||
}
|
||||
|
||||
public function testOnlyPatchException()
|
||||
{
|
||||
$this->expectException(ConfigException::class);
|
||||
ZMConfig::get('no_main_only_patch.test');
|
||||
}
|
||||
|
||||
public function testSmartPatch()
|
||||
{
|
||||
$array = [
|
||||
'key-1-1' => 'value-1-1',
|
||||
'key-1-2' => [
|
||||
'key-2-1' => [
|
||||
'key-3-1' => [
|
||||
'value-3-1',
|
||||
'value-3-2',
|
||||
],
|
||||
],
|
||||
],
|
||||
'key-1-3' => [
|
||||
'key-4-1' => 'value-4-1',
|
||||
],
|
||||
];
|
||||
$patch = [
|
||||
'key-1-2' => [
|
||||
'key-2-1' => [
|
||||
'key-3-1' => [
|
||||
'value-3-3',
|
||||
],
|
||||
],
|
||||
],
|
||||
'key-1-3' => [
|
||||
'key-4-2' => [
|
||||
'key-5-1' => 'value-5-1',
|
||||
],
|
||||
],
|
||||
];
|
||||
$expected = [
|
||||
'key-1-1' => 'value-1-1',
|
||||
'key-1-2' => [
|
||||
'key-2-1' => [
|
||||
'key-3-1' => [
|
||||
'value-3-3',
|
||||
],
|
||||
],
|
||||
],
|
||||
'key-1-3' => [
|
||||
'key-4-1' => 'value-4-1',
|
||||
'key-4-2' => [
|
||||
'key-5-1' => 'value-5-1',
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, ZMConfig::smartPatch($array, $patch));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user