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); }