From 18ae960f86d27f7248b5288910771bf72bc0ff9f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 May 2022 00:30:33 +0800 Subject: [PATCH 1/8] refactor ZMConfig --- composer.json | 1 - src/ZM/Config/ConfigMetadata.php | 18 ++ src/ZM/Config/ZMConfig.php | 292 +++++++++++++++++++++++++++ src/ZM/Exception/ConfigException.php | 15 ++ src/ZM/Framework.php | 13 +- src/ZM/Utils/DataProvider.php | 8 +- tests/ZM/Config/ZMConfigTest.php | 163 +++++++++++++++ tests/ZM/Utils/HttpUtilTest.php | 3 +- 8 files changed, 504 insertions(+), 9 deletions(-) create mode 100644 src/ZM/Config/ConfigMetadata.php create mode 100644 src/ZM/Config/ZMConfig.php create mode 100644 src/ZM/Exception/ConfigException.php create mode 100644 tests/ZM/Config/ZMConfigTest.php diff --git a/composer.json b/composer.json index 5fc87aee..85c330d4 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,6 @@ "symfony/polyfill-mbstring": "^1.19", "symfony/polyfill-php80": "^1.16", "symfony/routing": "~6.0 || ~5.0 || ~4.0 || ~3.0", - "zhamao/config": "^1.0", "zhamao/connection-manager": "^1.0", "zhamao/console": "^1.0", "zhamao/request": "^1.1" diff --git a/src/ZM/Config/ConfigMetadata.php b/src/ZM/Config/ConfigMetadata.php new file mode 100644 index 00000000..040263ca --- /dev/null +++ b/src/ZM/Config/ConfigMetadata.php @@ -0,0 +1,18 @@ + $v) { + if (!isset($data[$k])) { // 如果项目不在基类存在,则直接写入 + $data[$k] = $v; + } else { // 如果base存在的话,则递归patch覆盖 + $data[$k] = self::smartPatch($data[$k], $v); + } + } + return $data; + } + } + return $patch; + } + + /** + * @throws ConfigException + * @return array|int|string + */ + private static function loadConfig(string $name) + { + // 首先获取此名称的所有配置文件的路径 + self::parseList($name); + + $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 = DataProvider::scanDirFiles(self::$path, true, true); + foreach ($files as $file) { + Console::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) { + Console::warning('文件名 ' . $info['filename'] . ' 不合法(含有"."),请检查文件名是否合法。'); + continue; + } + $obj->path = realpath(self::$path . '/' . $info['dirname'] . '/' . $info['basename']); + $obj->extension = $ext; + $obj->data = self::readConfigFromFile(realpath(self::$path . '/' . $info['dirname'] . '/' . $info['basename']), $info['extension']); + $list[] = $obj; + } + } + // 如果是源码模式,config目录和default目录相同,所以不需要继续采摘default目录下的文件 + if (realpath(self::$path) !== realpath(self::DEFAULT_PATH)) { + $files = DataProvider::scanDirFiles(self::DEFAULT_PATH, true, true); + foreach ($files as $file) { + $info = pathinfo($file); + $info['extension'] = $info['extension'] ?? ''; + // 判断文件名是否为配置文件 + if (!in_array($info['extension'], self::SUPPORTED_EXTENSIONS)) { + 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(realpath(self::DEFAULT_PATH . '/' . $info['dirname'] . '/' . $info['basename']), $info['extension']); + $list[] = $obj; + } + } + } + self::$config_meta_list[$name] = $list; + } + + /** + * @param mixed $filename + * @param mixed $ext_name + * @throws ConfigException + */ + private static function readConfigFromFile($filename, $ext_name) + { + Console::debug('正加载配置文件 ' . $filename); + switch ($ext_name) { + case 'php': + $r = include_once $filename; + if ($r === true) { + // 已经加载过的文件,掐头直接eval读取 + $file_content = str_replace([' 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->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); + $this->assertStringContainsString('没读取过,正在从文件加载', $this->getActualOutput()); + } + + 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)); + } +} diff --git a/tests/ZM/Utils/HttpUtilTest.php b/tests/ZM/Utils/HttpUtilTest.php index 10d41be3..459e660a 100644 --- a/tests/ZM/Utils/HttpUtilTest.php +++ b/tests/ZM/Utils/HttpUtilTest.php @@ -10,6 +10,7 @@ use Swoole\Http\Response; use Symfony\Component\Routing\RouteCollection; use ZM\Annotation\Http\RequestMapping; use ZM\Annotation\Http\RequestMethod; +use ZM\Config\ZMConfig; use ZM\Utils\HttpUtil; use ZM\Utils\Manager\RouteManager; @@ -25,7 +26,7 @@ class HttpUtilTest extends TestCase { $swoole_response = $this->getMockClass(Response::class); $r = new \ZM\Http\Response(new $swoole_response()); - HttpUtil::handleStaticPage($page, $r); + HttpUtil::handleStaticPage($page, $r, ZMConfig::get('global', 'static_file_server')); $this->assertEquals($expected, $r->getStatusCode() === 200); } From 2478ffe331be7cc7330c3ec201cd27cdb4c964dc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 May 2022 00:31:03 +0800 Subject: [PATCH 2/8] fix WorkerStop read global config bug --- src/ZM/Event/SwooleEvent/OnWorkerStop.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ZM/Event/SwooleEvent/OnWorkerStop.php b/src/ZM/Event/SwooleEvent/OnWorkerStop.php index 507bee74..3ecd8e06 100644 --- a/src/ZM/Event/SwooleEvent/OnWorkerStop.php +++ b/src/ZM/Event/SwooleEvent/OnWorkerStop.php @@ -23,7 +23,7 @@ class OnWorkerStop implements SwooleEvent { WorkerContainer::getInstance()->flush(); - if ($worker_id == (ZMConfig::get('worker_cache')['worker'] ?? 0)) { + if ($worker_id == (ZMConfig::get('global.worker_cache.worker') ?? 0)) { LightCache::savePersistence(); } Console::verbose(($server->taskworker ? 'Task' : '') . "Worker #{$worker_id} 已停止 (Worker 状态码: " . $server->getWorkerStatus($worker_id) . ')'); From cc43993d5bd7f8422991e8b8f22c91ce94529449 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 May 2022 00:31:40 +0800 Subject: [PATCH 3/8] MessageUtilTest use smaller image to boost --- tests/ZM/Utils/MessageUtilTest.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/ZM/Utils/MessageUtilTest.php b/tests/ZM/Utils/MessageUtilTest.php index fa5d18ff..7b45fa70 100644 --- a/tests/ZM/Utils/MessageUtilTest.php +++ b/tests/ZM/Utils/MessageUtilTest.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace Tests\ZM\Utils; +use Exception; use PHPUnit\Framework\TestCase; use Throwable; use ZM\Annotation\CQ\CQCommand; -use ZM\API\CQ; use ZM\Event\EventManager; use ZM\Utils\DataProvider; use ZM\Utils\MessageUtil; @@ -17,6 +17,9 @@ use ZM\Utils\MessageUtil; */ class MessageUtilTest extends TestCase { + /** + * @throws Exception + */ public function testAddShortCommand(): void { EventManager::$events[CQCommand::class] = []; @@ -152,17 +155,17 @@ class MessageUtilTest extends TestCase */ public function testDownloadCQImage(): void { - if (file_exists(DataProvider::getDataFolder('images') . '/test.jpg')) { - unlink(DataProvider::getDataFolder('images') . '/test.jpg'); + if (file_exists(DataProvider::getDataFolder('images') . '/test.png')) { + unlink(DataProvider::getDataFolder('images') . '/test.png'); } - $msg = '[CQ:image,file=test.jpg,url=https://zhamao.xin/file/hello.jpg]'; + $msg = '[CQ:image,file=test.png,url=https://zhamao.xin/file/hello.png]'; try { $result = MessageUtil::downloadCQImage($msg); $this->assertIsArray($result); $this->assertCount(1, $result); - $this->assertFileExists(DataProvider::getDataFolder('images') . '/test.jpg'); - unlink(DataProvider::getDataFolder('images') . '/test.jpg'); + $this->assertFileExists(DataProvider::getDataFolder('images') . '/test.png'); + unlink(DataProvider::getDataFolder('images') . '/test.png'); } catch (Throwable $e) { if (strpos($e->getMessage(), 'enable-openssl') !== false) { $this->markTestSkipped('OpenSSL is not enabled'); From dcd6cae0f60ebe4307a7bc44469b96de236121f7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 May 2022 00:32:20 +0800 Subject: [PATCH 4/8] add global function `is_assoc_array` --- src/ZM/global_functions.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index e559550b..ae5504a7 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -776,6 +776,11 @@ function compare_object_and_array_by_keys(object $object, array $array, array $k return true; } +function is_assoc_array(array $array): bool +{ + return !empty($array) && array_keys($array) !== range(0, count($array) - 1); +} + /** * 以下为废弃的函数,将于未来移除 */ From 5f16766997dccfe4d112c30d8a6bac37aaeefa63 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 May 2022 00:32:42 +0800 Subject: [PATCH 5/8] update errcode docs --- docs/guide/errcode.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guide/errcode.md b/docs/guide/errcode.md index be68b41a..f0b9c0bf 100644 --- a/docs/guide/errcode.md +++ b/docs/guide/errcode.md @@ -79,4 +79,6 @@ | E00075 | Cron表达式非法 | 检查 @Cron 中的表达式是否书写格式正确 | | E00076 | Cron检查间隔非法 | 检查 @Cron 中的检查间隔(毫秒)是否在1000-60000之间 | | E00077 | 输入了非法的消息匹配参数类型 | 检查传入 `@CommandArgument` 或 `checkArguments()` 方法有没有传入非法的 `type` 参数 | +| E00078 | 配置文件读取失败,未找到 base 配置文件 | 检查配置文件是否存在(一般是由于没找到基类配置文件产生的此错误) | +| E00079 | 配置文件读取失败 | 根据报错信息修复配置文件(或将 debug 日志发给框架开发者或反馈 Issue) | | E99999 | 未知错误 | | From cbbac0554e4342558bf2ccc135c9b18d940fa7c0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 May 2022 01:04:18 +0800 Subject: [PATCH 6/8] fix Console debug for ZMConfig get --- src/ZM/Config/ZMConfig.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ZM/Config/ZMConfig.php b/src/ZM/Config/ZMConfig.php index 54f7ac1e..730efc46 100644 --- a/src/ZM/Config/ZMConfig.php +++ b/src/ZM/Config/ZMConfig.php @@ -82,7 +82,7 @@ class ZMConfig $head_name = array_shift($separated); // 首先判断有没有初始化这个配置文件,因为是只读,所以是懒加载,加载第一次后缓存起来 if (!isset(self::$config[$head_name])) { - Console::success('配置文件' . $name . ' ' . $additional_key . '没读取过,正在从文件加载 ...'); + Console::debug('配置文件' . $name . ' ' . $additional_key . '没读取过,正在从文件加载 ...'); self::$config[$head_name] = self::loadConfig($head_name); } // global.remote_terminal From d06b0dd6d60654beb0d82292e7443951b719b062 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 May 2022 14:53:23 +0800 Subject: [PATCH 7/8] change include_once to require --- src/ZM/Config/ZMConfig.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/ZM/Config/ZMConfig.php b/src/ZM/Config/ZMConfig.php index 730efc46..8a555860 100644 --- a/src/ZM/Config/ZMConfig.php +++ b/src/ZM/Config/ZMConfig.php @@ -268,14 +268,7 @@ class ZMConfig Console::debug('正加载配置文件 ' . $filename); switch ($ext_name) { case 'php': - $r = include_once $filename; - if ($r === true) { - // 已经加载过的文件,掐头直接eval读取 - $file_content = str_replace([' Date: Tue, 10 May 2022 16:03:37 +0800 Subject: [PATCH 8/8] change risky test --- tests/ZM/Config/ZMConfigTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ZM/Config/ZMConfigTest.php b/tests/ZM/Config/ZMConfigTest.php index de54d16e..782a7f85 100644 --- a/tests/ZM/Config/ZMConfigTest.php +++ b/tests/ZM/Config/ZMConfigTest.php @@ -48,12 +48,12 @@ class ZMConfigTest extends TestCase */ public function testReload() { + $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); - $this->assertStringContainsString('没读取过,正在从文件加载', $this->getActualOutput()); } public function testSetAndRestoreDirectory()