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/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 | 未知错误 | | 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 = 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反序列化失败,请检查文件内容'); + } + } +} 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) . ')'); diff --git a/src/ZM/Exception/ConfigException.php b/src/ZM/Exception/ConfigException.php new file mode 100644 index 00000000..d61d8f5e --- /dev/null +++ b/src/ZM/Exception/ConfigException.php @@ -0,0 +1,15 @@ + 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->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)); + } +} 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); } 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');