diff --git a/src/Globals/global_functions.php b/src/Globals/global_functions.php index 15c76222..862741d1 100644 --- a/src/Globals/global_functions.php +++ b/src/Globals/global_functions.php @@ -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 $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); +} diff --git a/src/ZM/Annotation/AnnotationParser.php b/src/ZM/Annotation/AnnotationParser.php index 70f6f285..c897baf5 100644 --- a/src/ZM/Annotation/AnnotationParser.php +++ b/src/ZM/Annotation/AnnotationParser.php @@ -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) { diff --git a/src/ZM/Command/CheckConfigCommand.php b/src/ZM/Command/CheckConfigCommand.php index ef49127b..eaec01e2 100644 --- a/src/ZM/Command/CheckConfigCommand.php +++ b/src/ZM/Command/CheckConfigCommand.php @@ -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])) { diff --git a/src/ZM/Command/Generate/SystemdGenerateCommand.php b/src/ZM/Command/Generate/SystemdGenerateCommand.php index 6de2711a..851f9ca1 100644 --- a/src/ZM/Command/Generate/SystemdGenerateCommand.php +++ b/src/ZM/Command/Generate/SystemdGenerateCommand.php @@ -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('成功生成 systemd 文件,位置:' . $path . ''); $output->writeln('有关如何使用 systemd 配置文件,请访问 `https://github.com/zhamao-robot/zhamao-framework/issues/36`'); diff --git a/src/ZM/Config/ConfigMetadata.php b/src/ZM/Config/ConfigMetadata.php deleted file mode 100644 index 040263ca..00000000 --- a/src/ZM/Config/ConfigMetadata.php +++ /dev/null @@ -1,18 +0,0 @@ -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); } } diff --git a/src/ZM/Container/ContainerServicesProvider.php b/src/ZM/Container/ContainerServicesProvider.php index 26a79e85..94d65d65 100644 --- a/src/ZM/Container/ContainerServicesProvider.php +++ b/src/ZM/Container/ContainerServicesProvider.php @@ -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和驱动 diff --git a/src/ZM/Event/Listener/MasterEventListener.php b/src/ZM/Event/Listener/MasterEventListener.php index 6ecd585a..8e6be230 100644 --- a/src/ZM/Event/Listener/MasterEventListener.php +++ b/src/ZM/Event/Listener/MasterEventListener.php @@ -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'], ]); }); diff --git a/src/ZM/Event/Listener/WorkerEventListener.php b/src/ZM/Event/Listener/WorkerEventListener.php index 2ca083ef..2df8156b 100644 --- a/src/ZM/Event/Listener/WorkerEventListener.php +++ b/src/ZM/Event/Listener/WorkerEventListener.php @@ -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) { diff --git a/src/ZM/Exception/ConfigException.php b/src/ZM/Exception/ConfigException.php index d61d8f5e..369f2d93 100644 --- a/src/ZM/Exception/ConfigException.php +++ b/src/ZM/Exception/ConfigException.php @@ -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}"); + } } diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index 73da85e4..af11cad8 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -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; } diff --git a/src/ZM/InstantApplication.php b/src/ZM/InstantApplication.php index 6076aee1..74c4e703 100644 --- a/src/ZM/InstantApplication.php +++ b/src/ZM/InstantApplication.php @@ -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; } diff --git a/src/ZM/Store/MySQL/MySQLDriver.php b/src/ZM/Store/MySQL/MySQLDriver.php index 2205248e..68815d0b 100644 --- a/src/ZM/Store/MySQL/MySQLDriver.php +++ b/src/ZM/Store/MySQL/MySQLDriver.php @@ -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) { diff --git a/src/ZM/Utils/HttpUtil.php b/src/ZM/Utils/HttpUtil.php index d10d0a64..df651c88 100644 --- a/src/ZM/Utils/HttpUtil.php +++ b/src/ZM/Utils/HttpUtil.php @@ -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)); } /** diff --git a/src/ZM/Utils/ReflectionUtil.php b/src/ZM/Utils/ReflectionUtil.php index 34a288a7..b3632e3f 100644 --- a/src/ZM/Utils/ReflectionUtil.php +++ b/src/ZM/Utils/ReflectionUtil.php @@ -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; + } } diff --git a/tests/ZM/Config/ZMConfigTest.php b/tests/ZM/Config/ZMConfigTest.php new file mode 100644 index 00000000..a2ca06fa --- /dev/null +++ b/tests/ZM/Config/ZMConfigTest.php @@ -0,0 +1,171 @@ + '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', ' "yes", "env" => "development"];' + ); + file_put_contents( + $mock_dir . '/test.production.php', + ' "yes", "env" => "production"];' + ); + file_put_contents( + $mock_dir . '/test.patch.php', + ' "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')); + } +} diff --git a/tests_old/ZM/Config/ZMConfigTest.php b/tests_old/ZM/Config/ZMConfigTest.php deleted file mode 100644 index 477c286c..00000000 --- a/tests_old/ZM/Config/ZMConfigTest.php +++ /dev/null @@ -1,164 +0,0 @@ - 30055];'); - file_put_contents($mock_dir . '/php_exception.php', ' 30055];'); - file_put_contents($mock_dir . '/global.invalid.development.php', ' 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)); - } -}