From b2c95d96b13f69998240148eb318b55af4f6ba72 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 13 Aug 2022 17:00:29 +0800 Subject: [PATCH] refactor all base things --- .github/workflows/integration-test.yml | 3 - composer.json | 3 - config/global.php | 23 + instant-demo.php | 26 - instant-plugin-demo.php | 22 + mybot.php | 39 + src/Globals/global_defines_app.php | 9 +- src/Globals/global_defines_framework.php | 7 +- src/Globals/global_functions.php | 78 +- src/Globals/script_setup_loader.php | 77 +- src/Module/Example/Hello123.php | 26 + src/ZM/Annotation/AnnotationBase.php | 11 + src/ZM/Annotation/AnnotationHandler.php | 154 ++-- src/ZM/Annotation/AnnotationMap.php | 16 +- src/ZM/Annotation/AnnotationParser.php | 255 +++--- src/ZM/Annotation/Closed.php | 4 +- src/ZM/Annotation/Framework/BindEvent.php | 53 ++ src/ZM/Annotation/Framework/Init.php | 29 + .../Framework/{OnSetup.php => Setup.php} | 3 +- src/ZM/Annotation/Http/Route.php | 5 + src/ZM/Annotation/OneBot/BotCommand.php | 146 ++++ .../{OnOneBotEvent.php => BotEvent.php} | 17 +- src/ZM/Annotation/OneBot/CommandArgument.php | 146 ++++ src/ZM/Command/Server/ServerStartCommand.php | 19 +- src/ZM/Config/ZMConfig.php | 20 +- src/ZM/ConsoleApplication.php | 9 +- src/ZM/Container/BoundMethod.php | 104 +++ src/ZM/Container/Container.php | 61 ++ src/ZM/Container/ContainerInterface.php | 110 +++ .../Container/ContainerServicesProvider.php | 121 +++ src/ZM/Container/ContainerTrait.php | 735 ++++++++++++++++++ src/ZM/Container/EntryNotFoundException.php | 12 + src/ZM/Container/EntryResolutionException.php | 12 + src/ZM/Container/WorkerContainer.php | 13 + src/ZM/Context/Context.php | 23 + src/ZM/Context/ContextInterface.php | 30 + src/ZM/Context/Trait/HttpTrait.php | 41 + src/ZM/Event/Listener/HttpEventListener.php | 72 +- .../Event/Listener/ManagerEventListener.php | 11 + src/ZM/Event/Listener/WorkerEventListener.php | 85 ++ src/ZM/Exception/InvalidArgumentException.php | 15 + src/ZM/Framework.php | 31 +- src/ZM/InstantApplication.php | 35 + src/ZM/Middleware/MiddlewareHandler.php | 204 +++++ src/ZM/Middleware/MiddlewareInterface.php | 9 + src/ZM/Middleware/TimerMiddleware.php | 28 + src/ZM/Plugin/InstantPlugin.php | 77 ++ src/ZM/Store/FileSystem.php | 5 +- src/ZM/Store/InternalGlobals.php | 19 - src/ZM/Utils/HttpUtil.php | 170 ++++ src/ZM/Utils/ReflectionUtil.php | 123 +++ src/ZM/Utils/ZMUtil.php | 7 + src/entry.php | 15 +- tests/ZM/Utils/HttpUtilTest.php | 24 - 54 files changed, 3009 insertions(+), 383 deletions(-) delete mode 100644 instant-demo.php create mode 100644 instant-plugin-demo.php create mode 100644 mybot.php create mode 100644 src/Module/Example/Hello123.php create mode 100644 src/ZM/Annotation/Framework/BindEvent.php create mode 100644 src/ZM/Annotation/Framework/Init.php rename src/ZM/Annotation/Framework/{OnSetup.php => Setup.php} (88%) create mode 100644 src/ZM/Annotation/OneBot/BotCommand.php rename src/ZM/Annotation/OneBot/{OnOneBotEvent.php => BotEvent.php} (73%) create mode 100644 src/ZM/Annotation/OneBot/CommandArgument.php create mode 100644 src/ZM/Container/BoundMethod.php create mode 100644 src/ZM/Container/Container.php create mode 100644 src/ZM/Container/ContainerInterface.php create mode 100644 src/ZM/Container/ContainerServicesProvider.php create mode 100644 src/ZM/Container/ContainerTrait.php create mode 100644 src/ZM/Container/EntryNotFoundException.php create mode 100644 src/ZM/Container/EntryResolutionException.php create mode 100644 src/ZM/Container/WorkerContainer.php create mode 100644 src/ZM/Context/Context.php create mode 100644 src/ZM/Context/ContextInterface.php create mode 100644 src/ZM/Context/Trait/HttpTrait.php create mode 100644 src/ZM/Exception/InvalidArgumentException.php create mode 100644 src/ZM/InstantApplication.php create mode 100644 src/ZM/Middleware/MiddlewareHandler.php create mode 100644 src/ZM/Middleware/MiddlewareInterface.php create mode 100644 src/ZM/Middleware/TimerMiddleware.php create mode 100644 src/ZM/Plugin/InstantPlugin.php delete mode 100644 src/ZM/Store/InternalGlobals.php create mode 100644 src/ZM/Utils/HttpUtil.php create mode 100644 src/ZM/Utils/ReflectionUtil.php diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index ace57c31..21645f83 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -63,9 +63,6 @@ jobs: - name: Run Static Analysis run: "composer analyse" - - name: Run PHPUnit - run: "composer test" - cs-check: name: PHP CS Fixer Check runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index 5cd0f875..6e74c515 100644 --- a/composer.json +++ b/composer.json @@ -20,14 +20,12 @@ "jelix/version": "^2.0", "koriym/attributes": "^1.0", "psr/container": "^2.0", - "psy/psysh": "^0.11.2", "symfony/console": "~6.0 || ~5.0 || ~4.0", "symfony/polyfill-ctype": "^1.19", "symfony/polyfill-mbstring": "^1.19", "symfony/polyfill-php80": "^1.16", "symfony/routing": "~6.0 || ~5.0 || ~4.0", "zhamao/logger": "dev-master", - "zhamao/request": "^1.1", "onebot/libonebot": "dev-develop" }, "require-dev": { @@ -63,7 +61,6 @@ "autoload-dev": { "psr-4": { "Module\\": "src/Module", - "Custom\\": "src/Custom", "Tests\\": "tests" } }, diff --git a/config/global.php b/config/global.php index ce72ac23..5790f328 100644 --- a/config/global.php +++ b/config/global.php @@ -45,6 +45,9 @@ $config['swoole_options'] = [ 'swoole_server_mode' => SWOOLE_PROCESS, // Swoole Server 启动模式,默认为 SWOOLE_PROCESS ]; +/* 默认存取炸毛数据的目录(相对目录时,代表WORKING_DIR下的目录,绝对目录按照绝对目录来) */ +$config['data_dir'] = 'zm_data'; + /* 框架本体运行时的一些可调配置 */ $config['runtime'] = [ 'reload_delay_time' => 800, @@ -57,4 +60,24 @@ $config['runtime'] = [ 'timezone' => 'Asia/Shanghai', ]; +/* 上下文接口类 implemented from ContextInterface */ +$config['context_class'] = \ZM\Context\Context::class; + +/* 允许加载插件形式 */ +$config['plugin'] = [ + 'enable' => true, + 'load_dir' => 'plugins', +]; + +/* 静态文件读取器 */ +$config['file_server'] = [ + 'enable' => true, + 'document_root' => $config['data_dir'] . '/public/', + 'document_index' => 'index.html', + 'document_code_page' => [ + '404' => '404.html', + '500' => '500.html', + ], +]; + return $config; diff --git a/instant-demo.php b/instant-demo.php deleted file mode 100644 index e869537f..00000000 --- a/instant-demo.php +++ /dev/null @@ -1,26 +0,0 @@ -onEvent(OnOpenEvent::class, ['connect_type' => 'qq'], function (ConnectionObject $conn) { - Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已连接!'); -}); - -$weather->onEvent(CQCommand::class, ['match' => '你好'], function () { - ctx()->reply('hello呀!'); -}); - -$app = new ZMServer('app-name'); -$app->addModule($weather); -$app->run(); diff --git a/instant-plugin-demo.php b/instant-plugin-demo.php new file mode 100644 index 00000000..7131fabe --- /dev/null +++ b/instant-plugin-demo.php @@ -0,0 +1,22 @@ +withArgument(name: 'arg1')->withMethod(function () { + ctx()->reply('test ok'); + }); + $event = BotEvent::make(type: 'message')->withMethod(function () { + }); + $plugin->addBotEvent($event); + $plugin->addBotCommand($cmd); + + $plugin->registerEvent(HttpRequestEvent::getName(), function (HttpRequestEvent $event) { + $event->withResponse(\OneBot\Http\HttpFactory::getInstance()->createResponse(503)); + }); + return $plugin; +}; +*/ diff --git a/mybot.php b/mybot.php new file mode 100644 index 00000000..c5fd2298 --- /dev/null +++ b/mybot.php @@ -0,0 +1,39 @@ +patchConfig([ + 'driver' => 'workerman', +]); +// 改变启动所需的参数 +$app->patchArgs([ + '--private-mode', +]); +// 如果有 Composer 依赖的插件,使用 enablePlugins 进行开启 +$app->enablePlugins([ + 'a', + 'b', + 'c', + 'd', +]); +// BotCommand 事件构造 +$cmd = \ZM\Annotation\OneBot\BotCommand::make('test')->withMethod(function () { + ctx()->reply('test ok'); +}); +$event = \ZM\Annotation\OneBot\BotEvent::make('message')->withMethod(function () { +}); +$app->addBotEvent($event); +$app->addBotCommand($cmd); + +$app->registerEvent(HttpRequestEvent::getName(), function (HttpRequestEvent $event) { + $event->withResponse(\OneBot\Http\HttpFactory::getInstance()->createResponse(503)); +}); + +$app->run(); diff --git a/src/Globals/global_defines_app.php b/src/Globals/global_defines_app.php index 608ed47c..676f9b28 100644 --- a/src/Globals/global_defines_app.php +++ b/src/Globals/global_defines_app.php @@ -25,9 +25,11 @@ const ZM_PROCESS_WORKER = ONEBOT_PROCESS_WORKER; const ZM_PROCESS_USER = ONEBOT_PROCESS_USER; const ZM_PROCESS_TASKWORKER = ONEBOT_PROCESS_TASKWORKER; -const ZM_PARSE_BEFORE_DRIVER = 0; -const ZM_PARSE_AFTER_DRIVER = 1; -const ZM_PARSE_BEFORE_START = 2; +/** 定义一些内部引用的错误ID */ +const ZM_ERR_NONE = 0; // 正常 +const ZM_ERR_METHOD_NOT_FOUND = 1; // 找不到方法 +const ZM_ERR_ROUTE_NOT_FOUND = 2; // 找不到路由 +const ZM_ERR_ROUTE_METHOD_NOT_ALLOWED = 3; // 路由方法不允许 /* 定义工作目录 */ define('WORKING_DIR', getcwd()); @@ -52,7 +54,6 @@ if (DIRECTORY_SEPARATOR !== '\\') { /* 对 global.php 在 Windows 下的兼容性考虑,因为 Windows 或者无 Swoole 环境时候无法运行 */ !defined('SWOOLE_BASE') && define('SWOOLE_BASE', 1) && define('SWOOLE_PROCESS', 2); - !defined('SWOOLE_HOOK_ALL') && ( define('SWOOLE_HOOK_TCP', 2) && define('SWOOLE_HOOK_UDP', 4) diff --git a/src/Globals/global_defines_framework.php b/src/Globals/global_defines_framework.php index dd59cdb0..48235af8 100644 --- a/src/Globals/global_defines_framework.php +++ b/src/Globals/global_defines_framework.php @@ -2,11 +2,14 @@ declare(strict_types=1); -/** 定义炸毛框架初始启动时间 */ +use ZM\Utils\ZMUtil; + +/* 定义炸毛框架初始启动时间 */ if (!defined('ZM_START_TIME')) { define('ZM_START_TIME', microtime(true)); } +/* 定义使用炸毛框架应用的版本 */ if (!defined('APP_VERSION')) { - define('APP_VERSION', LOAD_MODE == 1 ? (json_decode(file_get_contents(SOURCE_ROOT_DIR . '/composer.json'), true)['version'] ?? 'unknown') : 'unknown'); + define('APP_VERSION', LOAD_MODE == 1 ? (ZMUtil::getComposerMetadata()['version'] ?? ZM_VERSION) : ZM_VERSION); } diff --git a/src/Globals/global_functions.php b/src/Globals/global_functions.php index c404360d..c3b0503e 100644 --- a/src/Globals/global_functions.php +++ b/src/Globals/global_functions.php @@ -2,8 +2,18 @@ declare(strict_types=1); +use OneBot\V12\Object\MessageSegment; use Psr\Log\LoggerInterface; +use ZM\Container\Container; +use ZM\Container\ContainerInterface; +use ZM\Context\Context; use ZM\Logger\ConsoleLogger; +use ZM\Middleware\MiddlewareHandler; + +// 防止重复引用引发报错 +if (function_exists('zm_internal_errcode')) { + return; +} /** * 根据具体操作系统替换目录分隔符 @@ -61,12 +71,66 @@ function is_assoc_array(array $array): bool return !empty($array) && array_keys($array) !== range(0, count($array) - 1); } -/** - * @return object - * - * TODO: 等待完善DI - */ -function resolve(string $class) +function ctx(): Context { - return new $class(); + return \container()->get('ctx'); +} + +/** + * 构建消息段的助手函数 + * + * @param string $type 类型 + * @param array $data 字段 + */ +function segment(string $type, array $data = []): MessageSegment +{ + return new MessageSegment($type, $data); +} + +/** + * 中间件操作类的助手函数 + */ +function middleware(): MiddlewareHandler +{ + return MiddlewareHandler::getInstance(); +} + +// ////////////////// 容器部分 ////////////////////// + +/** + * 获取容器(请求级)实例 + */ +function container(): ContainerInterface +{ + return Container::getInstance(); +} + +/** + * 解析类实例(使用容器) + * + * @template T + * @param class-string $abstract + * @return Closure|mixed|T + * @noinspection PhpDocMissingThrowsInspection + */ +function resolve(string $abstract, array $parameters = []) +{ + /* @noinspection PhpUnhandledExceptionInspection */ + return Container::getInstance()->make($abstract, $parameters); +} + +/** + * 获取容器实例 + * + * @template T + * @param null|class-string $abstract + * @return Closure|ContainerInterface|mixed|T + */ +function app(string $abstract = null, array $parameters = []) +{ + if (is_null($abstract)) { + return container(); + } + + return resolve($abstract, $parameters); } diff --git a/src/Globals/script_setup_loader.php b/src/Globals/script_setup_loader.php index 56c29dc8..a32d1111 100644 --- a/src/Globals/script_setup_loader.php +++ b/src/Globals/script_setup_loader.php @@ -2,60 +2,43 @@ declare(strict_types=1); -use Doctrine\Common\Annotations\AnnotationReader; -use Koriym\Attributes\AttributeReader; -use Koriym\Attributes\DualReader; -use ZM\Annotation\Framework\OnSetup; -use ZM\ConsoleApplication; -use ZM\Exception\InitException; -use ZM\Store\FileSystem; +use ZM\Annotation\AnnotationParser; +use ZM\Annotation\Framework\Setup; +use ZM\Utils\ZMUtil; function _zm_setup_loader() { try { - try { - new ConsoleApplication('zhamao'); - } catch (InitException $e) { - } - $base_path = SOURCE_ROOT_DIR; - $scan_paths = []; - $composer = json_decode(file_get_contents($base_path . '/composer.json'), true); - $exclude_annotations = array_merge($composer['extra']['exclude_annotate'] ?? [], $composer['extra']['zm']['exclude-annotation-path'] ?? []); - foreach (($composer['autoload']['psr-4'] ?? []) as $k => $v) { - if (is_dir($base_path . '/' . $v) && !in_array($v, $exclude_annotations)) { - $scan_paths[trim($k, '\\')] = $base_path . '/' . $v; + global $_tmp_setup_list; + $_tmp_setup_list = []; + $parser = new AnnotationParser(false); + $composer = ZMUtil::getComposerMetadata(); + // 合并 dev 和 非 dev 的 psr-4 加载目录 + $merge_psr4 = array_merge($composer['autoload']['psr-4'] ?? [], $composer['autoload-dev']['psr-4'] ?? []); + // 排除 composer.json 中指定需要排除的目录 + $excludes = $composer['extra']['zm']['exclude-annotation-path'] ?? []; + foreach ($merge_psr4 as $k => $v) { + // 如果在排除表就排除,否则就解析注解 + if (is_dir(SOURCE_ROOT_DIR . '/' . $v) && !in_array($v, $excludes)) { + // 添加解析路径,对应Base命名空间也贴出来 + $parser->addRegisterPath(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\')); } } - foreach (($composer['autoload-dev']['psr-4'] ?? []) as $k => $v) { - if (is_dir($base_path . '/' . $v) && !in_array($v, $exclude_annotations)) { - $scan_paths[trim($k, '\\')] = $base_path . '/' . $v; - } - } - $all_event_class = []; - foreach ($scan_paths as $namespace => $autoload_path) { - $all_event_class = array_merge($all_event_class, FileSystem::getClassesPsr4($autoload_path, $namespace)); - } + $parser->addSpecialParser(Setup::class, function (Setup $setup) { + global $_tmp_setup_list; + $_tmp_setup_list[] = [ + 'class' => $setup->class, + 'method' => $setup->method, + ]; + return true; + }); - $reader = new DualReader(new AnnotationReader(), new AttributeReader()); - $event_list = []; - $setup_list = []; - foreach ($all_event_class as $v) { - $reflection_class = new ReflectionClass($v); - $methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC); - foreach ($methods as $vs) { - $method_annotations = $reader->getMethodAnnotations($vs); - if ($method_annotations != []) { - $annotation = $method_annotations[0]; - if ($annotation instanceof OnSetup) { - $setup_list[] = [ - 'class' => $v, - 'method' => $vs->getName(), - ]; - } - } - } - } - return json_encode(['setup' => $setup_list, 'event' => $event_list]); + // TODO: 然后加载插件目录下的插件 + + // 解析所有注册路径的文件,获取注解 + $parser->parseAll(); + + return json_encode(['setup' => $_tmp_setup_list]); } catch (Throwable $e) { $stderr = fopen('php://stderr', 'w'); fwrite($stderr, zm_internal_errcode('E00031') . $e->getMessage() . ' in ' . $e->getFile() . ' at line ' . $e->getLine() . PHP_EOL); diff --git a/src/Module/Example/Hello123.php b/src/Module/Example/Hello123.php new file mode 100644 index 00000000..972b7690 --- /dev/null +++ b/src/Module/Example/Hello123.php @@ -0,0 +1,26 @@ +method = $method; + return $this; + } + public function getIterator(): Traversable { return new ArrayIterator($this); diff --git a/src/ZM/Annotation/AnnotationHandler.php b/src/ZM/Annotation/AnnotationHandler.php index bb5fa594..7b74aec3 100644 --- a/src/ZM/Annotation/AnnotationHandler.php +++ b/src/ZM/Annotation/AnnotationHandler.php @@ -4,10 +4,9 @@ declare(strict_types=1); namespace ZM\Annotation; -use Generator; use Throwable; -use ZM\Annotation\Middleware\Middleware; use ZM\Exception\InterruptException; +use ZM\Middleware\MiddlewareHandler; /** * 注解调用器,原 EventDispatcher @@ -39,17 +38,34 @@ class AnnotationHandler /** @var mixed */ private $return_val; + /** + * 注解调用器构造函数 + * + * @param string $annotation_class 注解类名 + */ public function __construct(string $annotation_class) { $this->annotation_class = $annotation_class; logger()->debug('开始分发注解 {annotation}', ['annotation' => $annotation_class]); } + /** + * 立刻中断注解调用器执行 + * + * @param mixed $return_var 中断执行返回值,传入null则代表无返回值 + * @throws InterruptException + */ public static function interrupt($return_var = null) { throw new InterruptException($return_var); } + /** + * 设置执行前判断注解是否应该被执行的检查回调函数 + * + * @param callable $rule 回调函数 + * @return $this + */ public function setRuleCallback(callable $rule): AnnotationHandler { logger()->debug('注解调用器设置事件ruleFunc: {annotation}', ['annotation' => $this->annotation_class]); @@ -57,6 +73,12 @@ class AnnotationHandler return $this; } + /** + * 设置成功执行后有返回值时执行的返回值后续逻辑回调函数 + * + * @param callable $return 回调函数 + * @return $this + */ public function setReturnCallback(callable $return): AnnotationHandler { logger()->debug('注解调用器设置事件returnFunc: {annotation}', ['annotation' => $this->annotation_class]); @@ -65,119 +87,97 @@ class AnnotationHandler } /** - * @param mixed ...$params + * 调用注册了该注解的所有函数们 + * 此处会遍历所有注册了当前注解的函数,并支持中间件插入 + * + * @param mixed ...$params 传入的参数们 * @throws Throwable */ public function handleAll(...$params) { try { + // 遍历注册的注解 foreach ((AnnotationMap::$_list[$this->annotation_class] ?? []) as $v) { + // 调用单个注解 $this->handle($v, $this->rule_callback, ...$params); + // 执行完毕后检查状态,如果状态是规则判断或中间件before不通过,则重置状态后继续执行别的注解函数 if ($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) { $this->status = self::STATUS_NORMAL; continue; } + // 如果执行完毕,且设置了返回值后续逻辑的回调函数,那么就调用返回值回调的逻辑 if (is_callable($this->return_callback) && $this->status === self::STATUS_NORMAL) { ($this->return_callback)($this->return_val); } } } catch (InterruptException $e) { + // InterruptException 用于中断,这里必须 catch,并标记状态 $this->return_val = $e->return_var; $this->status = self::STATUS_INTERRUPTED; } catch (Throwable $e) { + // 其他类型的异常就顺势再抛出到外层,此层不做处理 $this->status = self::STATUS_EXCEPTION; throw $e; } } + /** + * 调用单个注解 + * + * @param mixed ...$params 传入的参数们 + * @throws InterruptException + * @throws Throwable + */ public function handle(AnnotationBase $v, ?callable $rule_callback = null, ...$params): bool { - $target_class = resolve($v->class); + // 由于3.0有额外的插件模式支持,所以注解就不再提供独立的闭包函数调用支持了 + // 提取要调用的目标类和方法名称 + $target_class = new ($v->class)(); $target_method = $v->method; - // 先执行规则 - if ($rule_callback !== null && !$rule_callback($this, $params)) { + // 先执行规则,失败就返回false + if ($rule_callback !== null && !$rule_callback($v, $params)) { $this->status = self::STATUS_RULE_FAILED; return false; } - - // 检查中间件 - $mid_obj = []; - $before_result = true; - foreach ($this->getRegisteredMiddlewares($target_class, $target_method) as $v) { - $mid_obj[] = $v[0]; // 投喂中间件 - if ($v[1] !== '') { // 顺带执行before - if (function_exists('container')) { - $before_result = container()->call([$v[0], $v[1]], $params); - } else { - $before_result = call_user_func([$v[0], $v[1]], $params); - } - if ($before_result === false) { - break; - } - } - } - $mid_obj_cnt1 = count($mid_obj) - 1; - if ($before_result) { // before全部通过了 - try { - // 执行注解绑定的方法 - // TODO: 记得完善好容器后把这里的这个if else去掉 - if (function_exists('container')) { - $this->return_val = container()->call([$target_class, $target_method], $params); - } else { - $this->return_val = call_user_func([$target_class, $target_method], $params); - } - } catch (Throwable $e) { - if ($e instanceof InterruptException) { - throw $e; - } - for ($i = $mid_obj_cnt1; $i >= 0; --$i) { - $obj = $mid_obj[$i]; - foreach ($obj[3] as $name => $method) { - if ($e instanceof $name) { - $obj[0]->{$method}($e); - return false; - } - } - } - throw $e; - } - } else { - $this->status = self::STATUS_BEFORE_FAILED; - } - for ($i = $mid_obj_cnt1; $i >= 0; --$i) { - if ($mid_obj[$i][2] !== '') { - $mid_obj[$i][0]->{$mid_obj[$i][2]}($this->return_val); + $callback = [$target_class, $target_method]; + try { + // 这块代码几乎等同于 middleware()->process() 中的内容,但由于注解调用器内含有一些特殊的特性(比如返回值回调),所以需要拆开来 + $before_result = middleware()->processBefore($callback, $params); + if ($before_result) { + // before都通过了,就执行本身,通过依赖注入执行 + // $this->return_val = container()->call($callback, $params); + $this->return_val = $callback(...$params); + } else { + // 没通过就标记是BEFORE_FAILED,然后接着执行after + $this->status = self::STATUS_BEFORE_FAILED; } + middleware()->processAfter($callback, $params); + } /* @noinspection PhpRedundantCatchClauseInspection */ catch (InterruptException $e) { + // 这里直接抛出这个异常的目的就是给上层handleAll()捕获 + throw $e; + } catch (Throwable $e) { + // 其余的异常就交给中间件的异常捕获器过一遍,没捕获的则继续抛出 + $this->status = self::STATUS_EXCEPTION; + MiddlewareHandler::getInstance()->processException($callback, $params, $e); } return true; } /** - * 获取注册过的中间件 - * - * @param object|string $class 类对象 - * @param string $method 方法名称 + * 获取分发的状态 */ - private function getRegisteredMiddlewares($class, string $method): Generator + public function getStatus(): int { - foreach (AnnotationMap::$_map[get_class($class)][$method] ?? [] as $annotation) { - if ($annotation instanceof Middleware) { - $name = $annotation->name; - $reg_mid = AnnotationMap::$_middleware_map[$name]['class'] ?? null; - if ($reg_mid === null) { - logger()->error('Not a valid middleware name: {name}', ['name' => $name]); - continue; - } + return $this->status; + } - $obj = new $reg_mid($annotation->params); - yield [ - $obj, - AnnotationMap::$_middleware_map[$name]['before'] ?? '', - AnnotationMap::$_middleware_map[$name]['after'] ?? '', - AnnotationMap::$_middleware_map[$name]['exceptions'] ?? [], - ]; - } - } - return []; + /** + * 获取运行的返回值 + * + * @return mixed + */ + public function getReturnVal() + { + return $this->return_val; } } diff --git a/src/ZM/Annotation/AnnotationMap.php b/src/ZM/Annotation/AnnotationMap.php index a10df802..65412a42 100644 --- a/src/ZM/Annotation/AnnotationMap.php +++ b/src/ZM/Annotation/AnnotationMap.php @@ -10,7 +10,7 @@ namespace ZM\Annotation; class AnnotationMap { /** - * 存取注解对象的列表 + * 存取注解对象的列表,key是注解类名,value是该注解对应的数组 * * @var array> * @internal @@ -18,14 +18,22 @@ class AnnotationMap public static $_list = []; /** + * 存取注解对象的三维列表,key1是注解所在的类名,key2是注解所在的方法名,value是该方法标注的注解们(数组) + * * @var array>> * @internal */ public static $_map = []; /** - * @var array - * @internal + * 将Parser解析后的注解注册到全局的 AnnotationMap + * + * @param AnnotationParser $parser 注解解析器 */ - public static $_middleware_map = []; + public static function loadAnnotationByParser(AnnotationParser $parser) + { + // 生成后加入到全局list中 + self::$_list = array_merge(self::$_list, $parser->generateAnnotationList()); + self::$_map = $parser->getAnnotationMap(); + } } diff --git a/src/ZM/Annotation/AnnotationParser.php b/src/ZM/Annotation/AnnotationParser.php index 79e5939c..70f6f285 100644 --- a/src/ZM/Annotation/AnnotationParser.php +++ b/src/ZM/Annotation/AnnotationParser.php @@ -10,64 +10,90 @@ use Koriym\Attributes\DualReader; use ReflectionClass; use ReflectionException; use ReflectionMethod; -use Symfony\Component\Routing\RouteCollection; use ZM\Annotation\Http\Controller; use ZM\Annotation\Http\Route; use ZM\Annotation\Interfaces\ErgodicAnnotation; use ZM\Annotation\Interfaces\Level; -use ZM\Annotation\Middleware\HandleAfter; -use ZM\Annotation\Middleware\HandleBefore; -use ZM\Annotation\Middleware\HandleException; use ZM\Annotation\Middleware\Middleware; -use ZM\Annotation\Middleware\MiddlewareClass; use ZM\Config\ZMConfig; use ZM\Exception\ConfigException; use ZM\Store\FileSystem; -use ZM\Store\InternalGlobals; +use ZM\Utils\HttpUtil; +/** + * 注解解析器 + */ class AnnotationParser { + /** + * @var array 要解析的路径列表 + */ private $path_list = []; + /** + * @var float 用于计算解析时间用的 + */ private $start_time; + /** + * @var array 用于解析的注解解析树,格式见下方的注释 + */ + private $annotation_tree = []; + + /** + * @var array 用于生成"类-方法"对应"注解列表"的数组 + */ private $annotation_map = []; - private $middleware_map = []; - - private $middlewares = []; - - /** @var null|AnnotationReader|DualReader */ - private $reader; - - private $req_mapping = []; + /** + * @var array 特殊的注解解析器回调列表 + */ + private $special_parsers = []; /** * AnnotationParser constructor. */ - public function __construct() + public function __construct(bool $with_internal_parsers = true) { $this->start_time = microtime(true); - // $this->loadAnnotationClasses(); - $this->req_mapping[0] = [ - 'id' => 0, - 'pid' => -1, - 'name' => '/', - ]; + + if ($with_internal_parsers) { + $this->special_parsers = [ + Middleware::class => [function (Middleware $middleware) { \middleware()->bindMiddleware([resolve($middleware->class), $middleware->method], $middleware->name, $middleware->params); }], + Route::class => [[$this, 'addRouteAnnotation']], + ]; + } + } + + /** + * 设置自定义的注解解析方法 + * + * @param string $class_name 注解类名 + * @param callable $callback 回调函数 + */ + public function addSpecialParser(string $class_name, callable $callback) + { + $this->special_parsers[$class_name][] = $callback; } /** * 注册各个模块类的注解和模块level的排序 + * * @throws ReflectionException * @throws ConfigException */ public function parseAll() { + // 对每个设置的路径依次解析 foreach ($this->path_list as $path) { logger()->debug('parsing annotation in ' . $path[0] . ':' . $path[1]); + + // 首先获取路径下所有的类(通过 PSR-4 标准解析) $all_class = FileSystem::getClassesPsr4($path[0], $path[1]); + // 读取配置文件中配置的忽略解析的注解名,防止误解析一些别的地方需要的注解,比如@mixin $conf = ZMConfig::get('global.runtime.annotation_reader_ignore'); + // 有两种方式,第一种是通过名称,第二种是通过命名空间 if (isset($conf['name']) && is_array($conf['name'])) { foreach ($conf['name'] as $v) { AnnotationReader::addGlobalIgnoredName($v); @@ -78,14 +104,18 @@ class AnnotationParser AnnotationReader::addGlobalIgnoredNamespace($v); } } + // 因为mixin常用,且框架默认不需要解析,则全局忽略 AnnotationReader::addGlobalIgnoredName('mixin'); - $this->reader = new DualReader(new AnnotationReader(), new AttributeReader()); + + // 声明一个既可以解析注解又可以解析Attribute的双reader来读取注解和Attribute + $reader = new DualReader(new AnnotationReader(), new AttributeReader()); foreach ($all_class as $v) { logger()->debug('正在检索 ' . $v); + // 通过反射实现注解读取 $reflection_class = new ReflectionClass($v); $methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC); - $class_annotations = $this->reader->getClassAnnotations($reflection_class); + $class_annotations = $reader->getClassAnnotations($reflection_class); // 这段为新加的:start // 这里将每个类里面所有的类注解、方法注解通通加到一颗大树上,后期解析 /* @@ -105,64 +135,85 @@ class AnnotationParser } */ - // 生成主树 - $this->annotation_map[$v]['class_annotations'] = $class_annotations; - $this->annotation_map[$v]['methods'] = $methods; + // 保存对class的注解 + $this->annotation_tree[$v]['class_annotations'] = $class_annotations; + // 保存类成员的方法的对应反射对象们 + $this->annotation_tree[$v]['methods'] = $methods; + // 保存对每个方法获取到的注解们 foreach ($methods as $method) { - $this->annotation_map[$v]['methods_annotations'][$method->getName()] = $this->reader->getMethodAnnotations($method); + $this->annotation_tree[$v]['methods_annotations'][$method->getName()] = $reader->getMethodAnnotations($method); } - foreach ($this->annotation_map[$v]['class_annotations'] as $vs) { + // 因为适用于类的注解有一些比较特殊,比如有向下注入的,有控制行为的,所以需要遍历一下下放到方法里 + foreach ($this->annotation_tree[$v]['class_annotations'] as $vs) { $vs->class = $v; - // 预处理1:将适用于每一个函数的注解到类注解重新注解到每个函数下面 - if (($vs instanceof ErgodicAnnotation) && ($vs instanceof AnnotationBase)) { - foreach (($this->annotation_map[$v]['methods'] ?? []) as $method) { + // 预处理0:排除所有非继承于 AnnotationBase 的注解 + if (!$vs instanceof AnnotationBase) { + logger()->notice(get_class($vs) . ' is not extended from ' . AnnotationBase::class); + continue; + } + + // 预处理1:如果类包含了@Closed注解,则跳过这个类 + if ($vs instanceof Closed) { + unset($this->annotation_tree[$v]); + continue 2; + } + + // 预处理2:将适用于每一个函数的注解到类注解重新注解到每个函数下面 + if ($vs instanceof ErgodicAnnotation) { + foreach (($this->annotation_tree[$v]['methods'] ?? []) as $method) { + // 用 clone 的目的是生成个独立的对象,避免和 class 以及方法之间互相冲突 $copy = clone $vs; $copy->method = $method->getName(); - $this->annotation_map[$v]['methods_annotations'][$method->getName()][] = $copy; + $this->annotation_tree[$v]['methods_annotations'][$method->getName()][] = $copy; } } - // 预处理2:处理 class 下面的注解 - if ($vs instanceof Closed) { - unset($this->annotation_map[$v]); - continue 2; - } - if ($vs instanceof MiddlewareClass) { - // 注册中间件本身的类,标记到 middlewares 属性中 - logger()->debug('正在注册中间件 ' . $reflection_class->getName()); - $rs = $this->registerMiddleware($vs, $reflection_class); - $this->middlewares[$rs['name']] = $rs; + // 预处理3:调用自定义解析器 + foreach (($this->special_parsers[get_class($vs)] ?? []) as $parser) { + $result = $parser($vs); + if ($result === true) { + continue 2; + } + if ($result === false) { + continue 3; + } } } - $inserted = []; - // 预处理3:处理每个函数上面的特殊注解,就是需要操作一些东西的 - foreach (($this->annotation_map[$v]['methods_annotations'] ?? []) as $method_name => $methods_annotations) { + foreach (($this->annotation_tree[$v]['methods_annotations'] ?? []) as $method_name => $methods_annotations) { foreach ($methods_annotations as $method_anno) { - /* @var AnnotationBase $method_anno */ + // 预处理3.0:排除所有非继承于 AnnotationBase 的注解 + if (!$method_anno instanceof AnnotationBase) { + logger()->notice('Binding annotation ' . get_class($method_anno) . ' to ' . $v . '::' . $method_name . ' is not extended from ' . AnnotationBase::class); + continue; + } + + // 预处理3.1:给所有注解对象绑定当前的类名和方法名 $method_anno->class = $v; $method_anno->method = $method_name; - if (!($method_anno instanceof Middleware) && ($middlewares = ZMConfig::get('global.global_middleware_binding')[get_class($method_anno)] ?? []) !== []) { - if (!isset($inserted[$v][$method_name])) { - // 在这里在其他中间件前插入插入全局的中间件 - foreach ($middlewares as $middleware) { - $mid_class = new Middleware($middleware); - $mid_class->class = $v; - $mid_class->method = $method_name; - $this->middleware_map[$v][$method_name][] = $mid_class; - } - $inserted[$v][$method_name] = true; - } - } elseif ($method_anno instanceof Route) { - $this->addRouteAnnotation($method_anno, $method_name, $v, $methods_annotations); - } elseif ($method_anno instanceof Middleware) { - $this->middleware_map[$method_anno->class][$method_anno->method][] = $method_anno; - } else { - AnnotationMap::$_map[$method_anno->class][$method_anno->method][] = $method_anno; + + // 预处理3.2:如果包含了@Closed注解,则跳过这个方法的注解解析 + if ($method_anno instanceof Closed) { + unset($this->annotation_tree[$v]['methods_annotations'][$method_name]); + continue 2; } + + // 预处理3.3:调用自定义解析器 + foreach (($this->special_parsers[get_class($method_anno)] ?? []) as $parser) { + $result = $parser($method_anno); + if ($result === true) { + continue 2; + } + if ($result === false) { + continue 3; + } + } + + // 如果上方没有解析或返回了 true,则添加到注解解析列表中 + $this->annotation_map[$v][$method_name][] = $method_anno; } } } @@ -170,10 +221,13 @@ class AnnotationParser logger()->debug('解析注解完毕!'); } - public function generateAnnotationEvents(): array + /** + * 生成排序后的注解列表 + */ + public function generateAnnotationList(): array { $o = []; - foreach ($this->annotation_map as $obj) { + foreach ($this->annotation_tree as $obj) { // 这里的ErgodicAnnotation是为了解决类上的注解可穿透到方法上的问题 foreach (($obj['class_annotations'] ?? []) as $class_annotation) { if ($class_annotation instanceof ErgodicAnnotation) { @@ -193,22 +247,9 @@ class AnnotationParser return $o; } - public function getMiddlewares(): array - { - return $this->middlewares; - } - - public function getMiddlewareMap(): array - { - return $this->middleware_map; - } - - public function getReqMapping(): array - { - return $this->req_mapping; - } - /** + * 添加解析的路径 + * * @param string $path 注册解析注解的路径 * @param string $indoor_name 起始命名空间的名称 */ @@ -219,6 +260,8 @@ class AnnotationParser } /** + * 排序注解列表 + * * @param array $events 需要排序的 * @param string $class_name 排序的类名 * @param string $prefix 前缀 @@ -229,53 +272,37 @@ class AnnotationParser if (is_a($class_name, Level::class, true)) { $class_name .= $prefix; usort($events[$class_name], function ($a, $b) { - $left = $a->level; - $right = $b->level; + $left = $a->getLevel(); + $right = $b->getLevel(); return $left > $right ? -1 : ($left == $right ? 0 : 1); }); } } - public function getUsedTime() + /** + * 获取解析器调用的时间(秒) + */ + public function getUsedTime(): float { return microtime(true) - $this->start_time; } - // private function below - - private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class): array + /** + * 获取注解的注册map + */ + public function getAnnotationMap(): array { - $result = [ - 'class' => '\\' . $reflection_class->getName(), - 'name' => $vs->name, - ]; - - foreach ($reflection_class->getMethods() as $vss) { - $method_annotations = $this->reader->getMethodAnnotations($vss); - foreach ($method_annotations as $vsss) { - if ($vsss instanceof HandleBefore) { - $result['before'] = $vss->getName(); - } - if ($vsss instanceof HandleAfter) { - $result['after'] = $vss->getName(); - } - if ($vsss instanceof HandleException) { - $result['exceptions'][$vsss->class_name] = $vss->getName(); - } - } - } - return $result; + return $this->annotation_map; } - private function addRouteAnnotation(Route $vss, $method, $class, $methods_annotations) + /** + * 添加注解路由 + */ + private function addRouteAnnotation(Route $vss) { - if (InternalGlobals::$routes === null) { - InternalGlobals::$routes = new RouteCollection(); - } - // 拿到所属方法的类上面有没有控制器的注解 $prefix = ''; - foreach ($methods_annotations as $annotation) { + foreach (($this->annotation_tree[$vss->class]['methods_annotations'][$vss->method] ?? []) as $annotation) { if ($annotation instanceof Controller) { $prefix = $annotation->prefix; break; @@ -284,9 +311,9 @@ class AnnotationParser $tail = trim($vss->route, '/'); $route_name = $prefix . ($tail === '' ? '' : '/') . $tail; logger()->debug('添加路由:' . $route_name); - $route = new \Symfony\Component\Routing\Route($route_name, ['_class' => $class, '_method' => $method]); + $route = new \Symfony\Component\Routing\Route($route_name, ['_class' => $vss->class, '_method' => $vss->method]); $route->setMethods($vss->request_method); - InternalGlobals::$routes->add(md5($route_name), $route); + HttpUtil::getRouteCollection()->add(md5($route_name), $route); } } diff --git a/src/ZM/Annotation/Closed.php b/src/ZM/Annotation/Closed.php index 82b0f10a..9b828f10 100644 --- a/src/ZM/Annotation/Closed.php +++ b/src/ZM/Annotation/Closed.php @@ -12,9 +12,9 @@ use Doctrine\Common\Annotations\Annotation\Target; * Class Closed * @Annotation * @NamedArgumentConstructor() - * @Target("CLASS") + * @Target("ALL") */ -#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)] class Closed extends AnnotationBase { } diff --git a/src/ZM/Annotation/Framework/BindEvent.php b/src/ZM/Annotation/Framework/BindEvent.php new file mode 100644 index 00000000..e9abfc19 --- /dev/null +++ b/src/ZM/Annotation/Framework/BindEvent.php @@ -0,0 +1,53 @@ +event_class = $event_class; + $this->level = $level; + } + + public function getLevel(): int + { + return $this->level; + } + + public function setLevel($level) + { + $this->level = $level; + } +} diff --git a/src/ZM/Annotation/Framework/Init.php b/src/ZM/Annotation/Framework/Init.php new file mode 100644 index 00000000..2ab13107 --- /dev/null +++ b/src/ZM/Annotation/Framework/Init.php @@ -0,0 +1,29 @@ +worker = $worker; + } +} diff --git a/src/ZM/Annotation/Framework/OnSetup.php b/src/ZM/Annotation/Framework/Setup.php similarity index 88% rename from src/ZM/Annotation/Framework/OnSetup.php rename to src/ZM/Annotation/Framework/Setup.php index daef9a59..d3a19d88 100644 --- a/src/ZM/Annotation/Framework/OnSetup.php +++ b/src/ZM/Annotation/Framework/Setup.php @@ -14,8 +14,9 @@ use ZM\Annotation\AnnotationBase; * @Annotation * @NamedArgumentConstructor() * @Target("METHOD") + * @since 3.0.0 */ #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)] -class OnSetup extends AnnotationBase +class Setup extends AnnotationBase { } diff --git a/src/ZM/Annotation/Http/Route.php b/src/ZM/Annotation/Http/Route.php index 9c51efa7..1ab8798b 100644 --- a/src/ZM/Annotation/Http/Route.php +++ b/src/ZM/Annotation/Http/Route.php @@ -48,4 +48,9 @@ class Route extends AnnotationBase $this->request_method = $request_method; $this->params = $params; } + + public static function make($route, $name = '', $request_method = ['GET', 'POST'], $params = []) + { + return new static($route, $name, $request_method, $params); + } } diff --git a/src/ZM/Annotation/OneBot/BotCommand.php b/src/ZM/Annotation/OneBot/BotCommand.php new file mode 100644 index 00000000..49448200 --- /dev/null +++ b/src/ZM/Annotation/OneBot/BotCommand.php @@ -0,0 +1,146 @@ +name = $name; + $this->match = $match; + $this->pattern = $pattern; + $this->regex = $regex; + $this->start_with = $start_with; + $this->end_with = $end_with; + $this->keyword = $keyword; + $this->alias = $alias; + $this->message_type = $message_type; + $this->user_id = $user_id; + $this->group_id = $group_id; + $this->level = $level; + } + + public static function make( + $name = '', + $match = '', + $pattern = '', + $regex = '', + $start_with = '', + $end_with = '', + $keyword = '', + $alias = [], + $message_type = '', + $user_id = '', + $group_id = '', + $level = 20 + ): BotCommand { + return new static(...func_get_args()); + } + + /** + * @throws InvalidArgumentException + * @throws ZMKnownException + * @return $this + */ + public function withArgument( + string $name, + string $description = '', + string $type = 'string', + bool $required = false, + string $prompt = '', + string $default = '', + int $timeout = 60, + int $error_prompt_policy = 1 + ): BotCommand { + $this->arguments[] = new CommandArgument($name, $description, $type, $required, $prompt, $default, $timeout, $error_prompt_policy); + return $this; + } + + public function getLevel(): int + { + return $this->level; + } + + /** + * @param int $level + */ + public function setLevel($level) + { + $this->level = $level; + } + + public function getArguments(): array + { + return $this->arguments; + } +} diff --git a/src/ZM/Annotation/OneBot/OnOneBotEvent.php b/src/ZM/Annotation/OneBot/BotEvent.php similarity index 73% rename from src/ZM/Annotation/OneBot/OnOneBotEvent.php rename to src/ZM/Annotation/OneBot/BotEvent.php index 472e8063..cbd839f8 100644 --- a/src/ZM/Annotation/OneBot/OnOneBotEvent.php +++ b/src/ZM/Annotation/OneBot/BotEvent.php @@ -10,12 +10,14 @@ use Doctrine\Common\Annotations\Annotation\Target; use ZM\Annotation\AnnotationBase; /** + * 机器人相关事件注解 + * * @Annotation * @Target("METHOD") - * @NamedArgumentConstructor + * @NamedArgumentConstructor() */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class OnOneBotEvent extends AnnotationBase +class BotEvent extends AnnotationBase { /** @var null|string */ public $type; @@ -50,4 +52,15 @@ class OnOneBotEvent extends AnnotationBase $this->self_id = $self_id; $this->sub_type = $sub_type; } + + public static function make( + ?string $type = null, + ?string $detail_type = null, + ?string $impl = null, + ?string $platform = null, + ?string $self_id = null, + ?string $sub_type = null + ): BotEvent { + return new static(...func_get_args()); + } } diff --git a/src/ZM/Annotation/OneBot/CommandArgument.php b/src/ZM/Annotation/OneBot/CommandArgument.php new file mode 100644 index 00000000..10f216bd --- /dev/null +++ b/src/ZM/Annotation/OneBot/CommandArgument.php @@ -0,0 +1,146 @@ +name = $name; + $this->description = $description; + $this->type = $this->fixTypeName($type); + $this->required = $required; + $this->prompt = $prompt; + $this->default = $default; + $this->timeout = $timeout; + $this->error_prompt_policy = $error_prompt_policy; + if ($this->type === 'bool') { + if ($this->default === '') { + $this->default = 'yes'; + } + if (!in_array($this->default, array_merge(TRUE_LIST, FALSE_LIST))) { + throw new InvalidArgumentException('CommandArgument参数 ' . $name . ' 类型传入类型应为布尔型,检测到非法的默认值 ' . $this->default); + } + } elseif ($this->type === 'number') { + if ($this->default === '') { + $this->default = '0'; + } + if (!is_numeric($this->default)) { + throw new InvalidArgumentException('CommandArgument参数 ' . $name . ' 类型传入类型应为数字型,检测到非法的默认值 ' . $this->default); + } + } + } + + public function getTypeErrorPrompt(): string + { + return '参数类型错误,请重新输入!'; + } + + public function getErrorQuitPrompt(): string + { + return '参数类型错误,停止输入!'; + } + + /** + * @throws ZMKnownException + */ + protected function fixTypeName(string $type): string + { + $table = [ + 'str' => 'string', + 'string' => 'string', + 'strings' => 'string', + 'byte' => 'string', + 'num' => 'number', + 'number' => 'number', + 'int' => 'number', + 'float' => 'number', + 'double' => 'number', + 'boolean' => 'bool', + 'bool' => 'bool', + 'true' => 'bool', + 'any' => 'any', + 'all' => 'any', + '*' => 'any', + ]; + if (array_key_exists($type, $table)) { + return $table[$type]; + } + throw new ZMKnownException(zm_internal_errcode('E00077') . 'Invalid argument type: ' . $type . ', only support any, string, number and bool !'); + } +} diff --git a/src/ZM/Command/Server/ServerStartCommand.php b/src/ZM/Command/Server/ServerStartCommand.php index eadcc1f9..fff41984 100644 --- a/src/ZM/Command/Server/ServerStartCommand.php +++ b/src/ZM/Command/Server/ServerStartCommand.php @@ -28,33 +28,16 @@ class ServerStartCommand extends ServerCommand { $this->setAliases(['server:start']); $this->setDefinition([ - new InputOption('debug-mode', 'D', null, '开启调试模式 (这将关闭协程化)'), new InputOption('config-dir', null, InputOption::VALUE_REQUIRED, '指定其他配置文件目录'), new InputOption('driver', null, InputOption::VALUE_REQUIRED, '指定驱动类型'), - new InputOption('log-debug', null, null, '调整消息等级到debug (log-level=4)'), new InputOption('log-level', null, InputOption::VALUE_REQUIRED, '调整消息等级到debug (log-level=4)'), - new InputOption('log-verbose', null, null, '调整消息等级到verbose (log-level=3)'), - new InputOption('log-info', null, null, '调整消息等级到info (log-level=2)'), - new InputOption('log-warning', null, null, '调整消息等级到warning (log-level=1)'), - new InputOption('log-error', null, null, '调整消息等级到error (log-level=0)'), - new InputOption('log-theme', null, InputOption::VALUE_REQUIRED, '改变终端的主题配色'), - new InputOption('disable-console-input', null, null, '禁止终端输入内容 (废弃)'), - new InputOption('interact', null, null, '打开终端输入'), - new InputOption('remote-terminal', null, null, '启用远程终端,配置使用global.php中的'), - new InputOption('disable-coroutine', null, null, '关闭协程Hook'), new InputOption('daemon', null, null, '以守护进程的方式运行框架'), new InputOption('worker-num', null, InputOption::VALUE_REQUIRED, '启动框架时运行的 Worker 进程数量'), - new InputOption('task-worker-num', null, InputOption::VALUE_REQUIRED, '启动框架时运行的 TaskWorker 进程数量'), new InputOption('watch', null, null, '监听 src/ 目录的文件变化并热更新'), - new InputOption('show-php-ver', null, null, '启动时显示PHP和Swoole版本'), new InputOption('env', null, InputOption::VALUE_REQUIRED, '设置环境类型 (production, development, staging)'), new InputOption('disable-safe-exit', null, null, '关闭安全退出(关闭后按CtrlC时直接杀死进程)'), - new InputOption('preview', null, null, '只显示参数,不启动服务器'), - new InputOption('force-load-module', null, InputOption::VALUE_OPTIONAL, '强制打包状态下加载模块(使用英文逗号分割多个)'), - new InputOption('polling-watch', null, null, '强制启用轮询模式监听'), new InputOption('no-state-check', null, null, '关闭启动前框架运行状态检查'), new InputOption('private-mode', null, null, '启动时隐藏MOTD和敏感信息'), - new InputOption('audit-mode', null, null, '启动时开启审计模式,独立将所有日志输出到文件供开发人员审计'), ]); $this->setDescription('Run zhamao-framework | 启动框架'); $this->setHelp('直接运行可以启动'); @@ -84,7 +67,7 @@ class ServerStartCommand extends ServerCommand } } } - (new Framework($input->getOptions()))->start(); + (new Framework($input->getOptions()))->init()->start(); return 0; } } diff --git a/src/ZM/Config/ZMConfig.php b/src/ZM/Config/ZMConfig.php index cec7110d..124c26a7 100644 --- a/src/ZM/Config/ZMConfig.php +++ b/src/ZM/Config/ZMConfig.php @@ -108,6 +108,13 @@ class ZMConfig self::$config_meta_list = []; } + /** + * 智能patch,将patch数组内的数据合并更新到data中 + * + * @param array|mixed $data 原数据 + * @param array|mixed $patch 要patch的数据 + * @return array|mixed + */ public static function smartPatch($data, $patch) { /* patch 样例: @@ -143,6 +150,8 @@ class ZMConfig } /** + * 加载配置文件 + * * @throws ConfigException * @return array|int|string */ @@ -182,6 +191,7 @@ class ZMConfig /** * 通过名称将所有该名称的配置文件路径和信息读取到列表中 + * * @throws ConfigException */ private static function parseList(string $name): void @@ -243,7 +253,7 @@ class ZMConfig if (!in_array($info['extension'], self::SUPPORTED_EXTENSIONS)) { continue; } - if ($info['filename'] === $name) { // 如果文件名与配置文件名一致 + if ($info['filename'] === $name) { // 如果文件名与配置文件名一致,就创建一个配置文件的元数据对象 $obj = new ConfigMetadata(); $obj->is_patch = false; $obj->is_env = false; @@ -258,11 +268,13 @@ class ZMConfig } /** - * @param mixed $filename - * @param mixed $ext_name + * 根据不同的扩展类型读取配置文件数组 + * + * @param mixed|string $filename 文件名 + * @param mixed|string $ext_name 扩展名 * @throws ConfigException */ - private static function readConfigFromFile($filename, $ext_name) + private static function readConfigFromFile($filename, $ext_name): array { logger()->debug('正加载配置文件 ' . $filename); switch ($ext_name) { diff --git a/src/ZM/ConsoleApplication.php b/src/ZM/ConsoleApplication.php index 810337b4..3ad0e44e 100644 --- a/src/ZM/ConsoleApplication.php +++ b/src/ZM/ConsoleApplication.php @@ -24,7 +24,7 @@ use ZM\Exception\InitException; * * 这里启动的不是框架,而是框架相关的命令行环境 */ -class ConsoleApplication extends Application +final class ConsoleApplication extends Application { private static $obj; @@ -36,13 +36,6 @@ class ConsoleApplication extends Application if (self::$obj !== null) { throw new InitException(zm_internal_errcode('E00069') . 'Initializing another Application is not allowed!'); } - // 如果已经有定义了全局的 WORKING_DIR,那么就报错 - // if (defined('WORKING_DIR')) { - // throw new InitException(); - // } - - // 启动前检查炸毛运行情况 - // _zm_env_check(); // 初始化命令 $this->add(new ServerStatusCommand()); // server运行状态 diff --git a/src/ZM/Container/BoundMethod.php b/src/ZM/Container/BoundMethod.php new file mode 100644 index 00000000..017d0082 --- /dev/null +++ b/src/ZM/Container/BoundMethod.php @@ -0,0 +1,104 @@ +make($callback[0]); + } + + if (!is_callable($callback)) { + throw new InvalidArgumentException('Callback is not callable.'); + } + + return call_user_func_array($callback, self::getMethodDependencies($container, $callback, $parameters)); + } + + /** + * Get all dependencies for a given method. + * + * @param callable|string $callback + * @throws ReflectionException + */ + protected static function getMethodDependencies(ContainerInterface $container, $callback, array $parameters = []): array + { + $dependencies = []; + + foreach (ReflectionUtil::getCallReflector($callback)->getParameters() as $i => $parameter) { + if (isset($parameters[$i]) && $parameter->hasType() && ($type = $parameter->getType())) { + if ($type instanceof \ReflectionNamedType && gettype($parameters[$i]) === $type->getName()) { + $dependencies[] = $parameters[$i]; + continue; + } + } + static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies); + } + + return array_merge($dependencies, array_values($parameters)); + } + + /** + * Get the dependency for the given call parameter. + * + * @throws EntryResolutionException + */ + protected static function addDependencyForCallParameter( + ContainerInterface $container, + ReflectionParameter $parameter, + array &$parameters, + array &$dependencies + ): void { + if (array_key_exists($param_name = $parameter->getName(), $parameters)) { + $dependencies[] = $parameters[$param_name]; + + unset($parameters[$param_name]); + } elseif (!is_null($class_name = ReflectionUtil::getParameterClassName($parameter))) { + if (array_key_exists($class_name, $parameters)) { + $dependencies[] = $parameters[$class_name]; + + unset($parameters[$class_name]); + } elseif ($parameter->isVariadic()) { + $variadic_dependencies = $container->make($class_name); + + $dependencies = array_merge($dependencies, is_array($variadic_dependencies) + ? $variadic_dependencies + : [$variadic_dependencies]); + } else { + $dependencies[] = $container->make($class_name); + } + } elseif ($parameter->isDefaultValueAvailable()) { + $dependencies[] = $parameter->getDefaultValue(); + } elseif (!array_key_exists($param_name, $parameters) && !$parameter->isOptional()) { + $message = "无法解析类 {$parameter->getDeclaringClass()->getName()} 的依赖 {$parameter}"; + + throw new EntryResolutionException($message); + } + } +} diff --git a/src/ZM/Container/Container.php b/src/ZM/Container/Container.php new file mode 100644 index 00000000..8dcbcf44 --- /dev/null +++ b/src/ZM/Container/Container.php @@ -0,0 +1,61 @@ +bound($id) || $this->getParent()->has($id); + } + + /** + * 获取一个绑定的实例 + * + * @template T + * @param class-string $abstract 类或接口名 + * @param array $parameters 参数 + * @throws EntryResolutionException + * @return Closure|mixed|T 实例 + */ + public function make(string $abstract, array $parameters = []) + { + if (isset($this->shared[$abstract])) { + return $this->shared[$abstract]; + } + + // 此类没有,父类有,则从父类中获取 + if (!$this->bound($abstract) && $this->getParent()->bound($abstract)) { + $this->log("{$abstract} is not bound, but in parent container, using parent container"); + return $this->getParent()->make($abstract, $parameters); + } + + return $this->traitMake($abstract, $parameters); + } +} diff --git a/src/ZM/Container/ContainerInterface.php b/src/ZM/Container/ContainerInterface.php new file mode 100644 index 00000000..26616c66 --- /dev/null +++ b/src/ZM/Container/ContainerInterface.php @@ -0,0 +1,110 @@ + $abstract 类或接口名 + * @param array $parameters 参数 + * @return Closure|mixed|T 实例 + */ + public function make(string $abstract, array $parameters = []); + + /** + * 调用对应的方法,并自动注入依赖 + * + * @param callable $callback 对应的方法 + * @param array $parameters 参数 + * @param null|string $default_method 默认方法 + * @return mixed + */ + public function call(callable $callback, array $parameters = [], string $default_method = null); +} diff --git a/src/ZM/Container/ContainerServicesProvider.php b/src/ZM/Container/ContainerServicesProvider.php new file mode 100644 index 00000000..3b837815 --- /dev/null +++ b/src/ZM/Container/ContainerServicesProvider.php @@ -0,0 +1,121 @@ +registerGlobalServices(WorkerContainer::getInstance()); + break; + case 'request': + $this->registerRequestServices(Container::getInstance(), ...$params); + break; + case 'message': + $this->registerConnectionServices(Container::getInstance()); + $this->registerMessageServices(Container::getInstance()); + break; + case 'connection': + $this->registerConnectionServices(Container::getInstance()); + break; + default: + break; + } + } + + /** + * 清理服务 + */ + public function cleanup(): void + { + container()->flush(); + } + + /** + * 注册全局服务 + * + * @throws ConfigException + */ + private function registerGlobalServices(ContainerInterface $container): void + { + // 注册路径类的容器快捷方式 + $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.framework', FRAMEWORK_ROOT_DIR); + + // 注册worker和驱动 + $container->instance('worker_id', ProcessManager::getProcessId()); + $container->instance(Driver::class, Framework::getInstance()->getDriver()); + + // 注册logger + $container->instance(LoggerInterface::class, logger()); + } + + /** + * 注册请求服务(HTTP请求) + */ + private function registerRequestServices(ContainerInterface $container, HttpRequestEvent $event): void + { + // $context = Context::$context[zm_cid()]; + $container->instance(HttpRequestEvent::class, $event); + $container->alias('http.request.event', HttpRequestEvent::class); + $container->instance(ServerRequestInterface::class, $event->getRequest()); + $container->alias('http.request', ServerRequestInterface::class); + // $container->instance(Request::class, $context['request']); + // $container->instance(Response::class, $context['response']); + $container->bind(ContextInterface::class, Context::class); + // $container->alias(ContextInterface::class, Context::class); + } + + /** + * 注册消息服务(WS消息) + */ + private function registerMessageServices(ContainerInterface $container): void + { + // $context = Context::$context[zm_cid()]; + // $container->instance(Frame::class, $context['frame']); // WS 消息帧 + // $container->bind(ContextInterface::class, Closure::fromCallable('ctx')); + // $container->alias(ContextInterface::class, Context::class); + } + + /** + * 注册链接服务 + */ + private function registerConnectionServices(ContainerInterface $container): void + { + // $context = Context::$context[zm_cid()]; + // $container->instance(ConnectionObject::class, $context['connection']); + } +} diff --git a/src/ZM/Container/ContainerTrait.php b/src/ZM/Container/ContainerTrait.php new file mode 100644 index 00000000..a79eef7d --- /dev/null +++ b/src/ZM/Container/ContainerTrait.php @@ -0,0 +1,735 @@ +shouldLog()) { + $this->log('Container created'); + } + } + + /** + * 判断对应的类或接口是否已经注册 + * + * @param string $abstract 类或接口名 + */ + public function bound(string $abstract): bool + { + return array_key_exists($abstract, self::$bindings) + || array_key_exists($abstract, self::$instances) + || array_key_exists($abstract, $this->shared) + || $this->isAlias($abstract); + } + + /** + * 获取类别名(如存在) + * + * @param string $abstract 类或接口名 + * @return string 别名,不存在时返回传入的类或接口名 + */ + public function getAlias(string $abstract): string + { + if (!isset(self::$aliases[$abstract])) { + return $abstract; + } + + return $this->getAlias(self::$aliases[$abstract]); + } + + /** + * 注册一个类别名 + * + * @param string $abstract 类或接口名 + * @param string $alias 别名 + */ + public function alias(string $abstract, string $alias): void + { + if ($alias === $abstract) { + throw new InvalidArgumentException("[{$abstract}] is same as [{$alias}]"); + } + + self::$aliases[$alias] = $abstract; + + if ($this->shouldLog()) { + $this->log("[{$abstract}] is aliased as [{$alias}]"); + } + } + + /** + * 注册绑定 + * + * @param string $abstract 类或接口名 + * @param null|Closure|string $concrete 返回类实例的闭包,或是类名 + * @param bool $shared 是否共享 + */ + public function bind(string $abstract, $concrete = null, bool $shared = false): void + { + $this->dropStaleInstances($abstract); + + // 如果没有提供闭包,则默认为自动解析类名 + if (is_null($concrete)) { + $concrete = $abstract; + } + + $concrete_name = ''; + if ($this->shouldLog()) { + $concrete_name = ReflectionUtil::variableToString($concrete); + } + + // 如果不是闭包,则认为是类名,此时将其包装在一个闭包中,以方便后续处理 + if (!$concrete instanceof Closure) { + $concrete = $this->getClosure($abstract, $concrete); + } + + self::$bindings[$abstract] = compact('concrete', 'shared'); + + if ($this->shouldLog()) { + $this->log("[{$abstract}] is bound to [{$concrete_name}]" . ($shared ? ' (shared)' : '')); + } + } + + /** + * 注册绑定 + * + * 在已经绑定时不会重复注册 + * + * @param string $abstract 类或接口名 + * @param null|Closure|string $concrete 返回类实例的闭包,或是类名 + * @param bool $shared 是否共享 + */ + public function bindIf(string $abstract, $concrete = null, bool $shared = false): void + { + if (!$this->bound($abstract)) { + $this->bind($abstract, $concrete, $shared); + } + } + + /** + * 注册一个单例绑定 + * + * @param string $abstract 类或接口名 + * @param null|Closure|string $concrete 返回类实例的闭包,或是类名 + */ + public function singleton(string $abstract, $concrete = null): void + { + $this->bind($abstract, $concrete, true); + } + + /** + * 注册一个单例绑定 + * + * 在已经绑定时不会重复注册 + * + * @param string $abstract 类或接口名 + * @param null|Closure|string $concrete 返回类实例的闭包,或是类名 + */ + public function singletonIf(string $abstract, $concrete = null): void + { + if (!$this->bound($abstract)) { + $this->singleton($abstract, $concrete); + } + } + + /** + * 注册一个已有的实例,效果等同于单例绑定 + * + * @param string $abstract 类或接口名 + * @param mixed $instance 实例 + * @return mixed + */ + public function instance(string $abstract, $instance) + { + if (isset(self::$instances[$abstract])) { + return self::$instances[$abstract]; + } + + self::$instances[$abstract] = $instance; + + if ($this->shouldLog()) { + $class_name = ReflectionUtil::variableToString($instance); + $this->log("[{$abstract}] is bound to [{$class_name}] (instance)"); + } + + return $instance; + } + + /** + * 获取一个解析对应类实例的闭包 + * + * @param string $abstract 类或接口名 + */ + public function factory(string $abstract): Closure + { + return function () use ($abstract) { + return $this->make($abstract); + }; + } + + /** + * 清除所有绑定和实例 + */ + public function flush(): void + { + self::$aliases = []; + self::$bindings = []; + self::$instances = []; + + $this->shared = []; + $this->build_stack = []; + $this->with = []; + + if ($this->shouldLog()) { + $this->log('Container flushed'); + } + } + + /** + * 获取一个绑定的实例 + * + * @template T + * @param class-string $abstract 类或接口名 + * @param array $parameters 参数 + * @throws EntryResolutionException + * @return Closure|mixed|T 实例 + */ + public function make(string $abstract, array $parameters = []) + { + $abstract = $this->getAlias($abstract); + + $needs_contextual_build = !empty($parameters); + + if (isset($this->shared[$abstract])) { + if ($this->shouldLog()) { + $this->log(sprintf( + '[%s] resolved (shared)%s', + $abstract, + $needs_contextual_build ? ' with ' . implode(', ', $parameters) : '' + )); + } + return $this->shared[$abstract]; + } + + // 如果已经存在在实例池中(通常意味着单例绑定),则直接返回该实例 + if (isset(self::$instances[$abstract]) && !$needs_contextual_build) { + if ($this->shouldLog()) { + $this->log("[{$abstract}] resolved (instance)"); + } + return self::$instances[$abstract]; + } + + $this->with[] = $parameters; + + $concrete = $this->getConcrete($abstract); + + // 构造该类的实例,并递归解析所有依赖 + if ($this->isBuildable($concrete, $abstract)) { + $object = $this->build($concrete); + } else { + $object = $this->make($concrete); + } + + // 如果该类存在扩展器(装饰器),则逐个应用到实例 + foreach ($this->getExtenders($abstract) as $extender) { + $object = $extender($object, $this); + } + + // 如果该类被注册为单例,则需要将其存放在实例池中,方便后续取用同一实例 + if (!$needs_contextual_build && $this->isShared($abstract)) { + $this->shared[$abstract] = $object; + if ($this->shouldLog()) { + $this->log("[{$abstract}] added to shared pool"); + } + } + + // 弹出本次构造的覆盖参数 + array_pop($this->with); + + if ($this->shouldLog()) { + $this->log(sprintf( + '[%s] resolved%s', + $abstract, + $needs_contextual_build ? ' with ' . implode(', ', $parameters) : '' + )); + } + + return $object; + } + + /** + * 实例化具体的类实例 + * + * @param Closure|string $concrete 类名或对应的闭包 + * @throws EntryResolutionException + * @return mixed + */ + public function build($concrete) + { + // 如果传入的是闭包,则直接执行并返回 + if ($concrete instanceof Closure) { + return $concrete($this, $this->getLastParameterOverride()); + } + + try { + $reflection = new ReflectionClass($concrete); + } catch (ReflectionException $e) { + throw new EntryResolutionException("指定的类 {$concrete} 不存在", 0, $e); + } + + if (!$reflection->isInstantiable()) { + $this->notInstantiable($concrete); + } + + $this->build_stack[] = $concrete; + + $constructor = $reflection->getConstructor(); + + // 如果不存在构造函数,则代表不需要进一步解析,直接实例化即可 + if (is_null($constructor)) { + array_pop($this->build_stack); + return new $concrete(); + } + + $dependencies = $constructor->getParameters(); + + // 获取所有依赖的实例 + try { + $instances = $this->resolveDependencies($dependencies); + } catch (EntryResolutionException $e) { + array_pop($this->build_stack); + throw $e; + } + + array_pop($this->build_stack); + + return $reflection->newInstanceArgs($instances); + } + + /** + * 调用对应的方法,并自动注入依赖 + * + * @param callable|string $callback 对应的方法 + * @param array $parameters 参数 + * @param null|string $default_method 默认方法 + * @return mixed + */ + public function call($callback, array $parameters = [], string $default_method = null) + { + if ($this->shouldLog()) { + if (count($parameters)) { + $str_parameters = array_map([ReflectionUtil::class, 'variableToString'], $parameters); + $str_parameters = implode(', ', $str_parameters); + } else { + $str_parameters = ''; + } + $this->log(sprintf( + 'Called %s%s(%s)', + ReflectionUtil::variableToString($callback), + $default_method ? '@' . $default_method : '', + $str_parameters + )); + } + return BoundMethod::call($this, $callback, $parameters, $default_method); + } + + /** + * Finds an entry of the container by its identifier and returns it. + * + * @param string $id identifier of the entry to look for + * + * @throws NotFoundExceptionInterface no entry was found for **this** identifier + * @throws ContainerExceptionInterface error while retrieving the entry + * + * @return mixed entry + */ + public function get(string $id) + { + try { + return $this->make($id); + } catch (Exception $e) { + if ($this->has($id)) { + throw new EntryResolutionException('', 0, $e); + } + + throw new EntryNotFoundException('', 0, $e); + } + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * `has($id)` returning true does not mean that `get($id)` will not throw an exception. + * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. + * + * @param string $id identifier of the entry to look for + */ + public function has(string $id): bool + { + return $this->bound($id); + } + + /** + * 扩展一个类或接口 + * + * @param string $abstract 类或接口名 + * @param Closure $closure 扩展闭包 + */ + public function extend(string $abstract, Closure $closure): void + { + $abstract = $this->getAlias($abstract); + + // 如果该类已经被解析过,则直接将扩展器应用到该类的实例上 + // 否则,将扩展器存入扩展器池,等待解析 + if (isset(self::$instances[$abstract])) { + self::$instances[$abstract] = $closure(self::$instances[$abstract], $this); + } else { + self::$extenders[$abstract][] = $closure; + } + + if ($this->shouldLog()) { + $this->log("[{$abstract}] extended"); + } + } + + /** + * 获取日志前缀 + */ + public function getLogPrefix(): string + { + return ($this->log_prefix ?: '[WorkerContainer(U)]') . ' '; + } + + /** + * 设置日志前缀 + */ + public function setLogPrefix(string $prefix): void + { + $this->log_prefix = $prefix; + } + + /** + * 获取对应类型的所有扩展器 + * + * @param string $abstract 类或接口名 + * @return Closure[] + */ + protected function getExtenders(string $abstract): array + { + $abstract = $this->getAlias($abstract); + + return self::$extenders[$abstract] ?? []; + } + + /** + * 判断传入的是否为别名 + */ + protected function isAlias(string $name): bool + { + return array_key_exists($name, self::$aliases); + } + + /** + * 抛弃所有过时的实例和别名 + * + * @param string $abstract 类或接口名 + */ + protected function dropStaleInstances(string $abstract): void + { + unset( + self::$instances[$abstract], + self::$aliases[$abstract], + $this->shared[$abstract] + ); + } + + /** + * 获取一个解析对应类的闭包 + * + * @param string $abstract 类或接口名 + * @param string $concrete 实际类名 + */ + protected function getClosure(string $abstract, string $concrete): Closure + { + return static function ($container, $parameters = []) use ($abstract, $concrete) { + $method = $abstract === $concrete ? 'build' : 'make'; + + return $container->{$method}($concrete, $parameters); + }; + } + + /** + * 获取最后一次的覆盖参数 + */ + protected function getLastParameterOverride(): array + { + return $this->with[count($this->with) - 1] ?? []; + } + + /** + * 抛出实例化异常 + * + * @throws EntryResolutionException + */ + protected function notInstantiable(string $concrete, string $reason = ''): void + { + if (!empty($this->build_stack)) { + $previous = implode(', ', $this->build_stack); + $message = "类 {$concrete} 无法实例化,其被 {$previous} 依赖"; + } else { + $message = "类 {$concrete} 无法实例化"; + } + + throw new EntryResolutionException("{$message}:{$reason}"); + } + + /** + * 解析依赖 + * + * @param ReflectionParameter[] $dependencies + * @throws EntryResolutionException + */ + protected function resolveDependencies(array $dependencies): array + { + $results = []; + + foreach ($dependencies as $dependency) { + // 如果此依赖存在覆盖参数,则使用覆盖参数 + // 否则,将尝试解析参数 + if ($this->hasParameterOverride($dependency)) { + $results[] = $this->getParameterOverride($dependency); + continue; + } + + // 如果存在临时注入的依赖,则使用临时注入的依赖 + if ($this->hasParameterTypeOverride($dependency)) { + $results[] = $this->getParameterTypeOverride($dependency); + continue; + } + + // 如果类名为空,则代表此依赖是基本类型,且无法对其进行依赖解析 + $class_name = ReflectionUtil::getParameterClassName($dependency); + $results[] = is_null($class_name) + ? $this->resolvePrimitive($dependency) + : $this->resolveClass($dependency); + + if ($this->shouldLog()) { + if (is_null($class_name)) { + if ($dependency->hasType()) { + $class_name = $dependency->getType(); + } else { + $class_name = 'Primitive'; + } + } + $this->log("Dependency [{$class_name} {$dependency->name}] resolved"); + } + } + + return $results; + } + + /** + * 判断传入的参数是否存在覆盖参数 + */ + protected function hasParameterOverride(ReflectionParameter $parameter): bool + { + return array_key_exists($parameter->name, $this->getLastParameterOverride()); + } + + /** + * 获取覆盖参数 + * + * @return mixed + */ + protected function getParameterOverride(ReflectionParameter $parameter) + { + return $this->getLastParameterOverride()[$parameter->name]; + } + + /** + * 判断传入的参数是否存在临时注入的参数 + */ + protected function hasParameterTypeOverride(ReflectionParameter $parameter): bool + { + if (!$parameter->hasType()) { + return false; + } + + $type = $parameter->getType(); + + if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) { + return false; + } + + return array_key_exists($type->getName(), $this->getLastParameterOverride()); + } + + /** + * 获取临时注入的参数 + * + * @return mixed + */ + protected function getParameterTypeOverride(ReflectionParameter $parameter) + { + $type = $parameter->getType(); + + if (!$type instanceof ReflectionNamedType) { + return []; + } + + return $this->getLastParameterOverride()[$type->getName()]; + } + + /** + * 解析基本类型 + * + * @throws EntryResolutionException 如参数不存在默认值,则抛出异常 + * @return mixed 对应类型的默认值 + */ + protected function resolvePrimitive(ReflectionParameter $parameter) + { + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + + throw new EntryResolutionException("无法解析类 {$parameter->getDeclaringClass()->getName()} 的参数 {$parameter}"); + } + + /** + * 解析类 + * + * @throws EntryResolutionException 如果无法解析类,则抛出异常 + * @return mixed + */ + protected function resolveClass(ReflectionParameter $parameter) + { + try { + // 尝试解析 + return $this->make(ReflectionUtil::getParameterClassName($parameter)); + } catch (EntryResolutionException $e) { + // 如果参数是可选的,则返回默认值 + if ($parameter->isDefaultValueAvailable()) { + array_pop($this->with); + return $parameter->getDefaultValue(); + } + + if ($parameter->isVariadic()) { + array_pop($this->with); + return []; + } + + throw $e; + } + } + + /** + * 获取类名的实际类型 + * + * @param string $abstract 类或接口名 + * @return Closure|string + */ + protected function getConcrete(string $abstract) + { + if (isset(self::$bindings[$abstract])) { + return self::$bindings[$abstract]['concrete']; + } + + return $abstract; + } + + /** + * 判断传入的实际类型是否可以构造 + * + * @param mixed $concrete 实际类型 + * @param string $abstract 类或接口名 + */ + protected function isBuildable($concrete, string $abstract): bool + { + return $concrete === $abstract || $concrete instanceof Closure; + } + + /** + * 判断传入的类型是否为共享实例 + * + * @param string $abstract 类或接口名 + */ + protected function isShared(string $abstract): bool + { + return isset($this->instances[$abstract]) + || (isset($this->bindings[$abstract]['shared']) + && $this->bindings[$abstract]['shared'] === true); + } + + /** + * 判断是否输出日志 + */ + protected function shouldLog(): bool + { + return true; + } + + /** + * 记录日志(自动附加容器日志前缀) + */ + protected function log(string $message): void + { + logger()->debug($this->getLogPrefix() . $message); + } +} diff --git a/src/ZM/Container/EntryNotFoundException.php b/src/ZM/Container/EntryNotFoundException.php new file mode 100644 index 00000000..03332d17 --- /dev/null +++ b/src/ZM/Container/EntryNotFoundException.php @@ -0,0 +1,12 @@ +get('http.request'); + } + + /** + * {@inheritDoc} + */ + public function getHttpRequestEvent(): HttpRequestEvent + { + $obj = container()->get('http.request.event'); + if (!$obj instanceof HttpRequestEvent) { + throw new ZMKnownException('E00099', 'current context container event is not HttpRequestEvent'); + } + return $obj; + } + + /** + * {@inheritDoc} + */ + public function withResponse(ResponseInterface $response) + { + $this->getHttpRequestEvent()->withResponse($response); + } +} diff --git a/src/ZM/Event/Listener/HttpEventListener.php b/src/ZM/Event/Listener/HttpEventListener.php index c684547f..fd31571a 100644 --- a/src/ZM/Event/Listener/HttpEventListener.php +++ b/src/ZM/Event/Listener/HttpEventListener.php @@ -5,21 +5,81 @@ declare(strict_types=1); namespace ZM\Event\Listener; use OneBot\Driver\Event\Http\HttpRequestEvent; -use OneBot\Driver\Event\StopException; use OneBot\Http\HttpFactory; +use OneBot\Http\Stream; use OneBot\Util\Singleton; +use Stringable; +use Throwable; +use ZM\Annotation\AnnotationHandler; +use ZM\Annotation\Framework\BindEvent; +use ZM\Annotation\Http\Route; +use ZM\Container\ContainerServicesProvider; +use ZM\Exception\ConfigException; +use ZM\Utils\HttpUtil; class HttpEventListener { use Singleton; /** - * @throws StopException + * 框架自身要实现的 HttpRequestEvent 事件回调 + * 这里处理框架特有的内容,比如: + * 路由、断点续传、注解再分发等 + * + * @throws Throwable */ - public function onRequest(HttpRequestEvent $event) + public function onRequest999(HttpRequestEvent $event) { - $msg = 'Hello from ' . $event->getSocketFlag(); - $res = HttpFactory::getInstance()->createResponse()->withBody(HttpFactory::getInstance()->createStream($msg)); - $event->withResponse($res); + // 注册容器 + resolve(ContainerServicesProvider::class)->registerServices('request', $event); + // 跑一遍 BindEvent 绑定了 HttpRequestEvent 的注解 + $handler = new AnnotationHandler(BindEvent::class); + $handler->setRuleCallback(function (BindEvent $anno) { + return $anno->event_class === HttpRequestEvent::class; + }); + $handler->handleAll($event); + // dump($event->getResponse()); + $node = null; + $params = null; + // 如果状态是 Normal,那么说明跑了一遍没有阻塞或者其他的情况,我就直接跑一遍内部的路由分发和静态文件分发 + if ($handler->getStatus() === AnnotationHandler::STATUS_NORMAL && $event->getResponse() === null) { + // 解析路由和路由状态 + $result = HttpUtil::parseUri($event->getRequest(), $node, $params); + switch ($result) { + case ZM_ERR_NONE: // 解析到存在路由了 + $handler = new AnnotationHandler(Route::class); + $div = new Route($node['route']); + $div->params = $params; + $div->method = $node['method']; + $div->request_method = $node['request_method']; + $div->class = $node['class']; + $starttime = microtime(true); + $handler->handle($div, null, $params, $event->getRequest(), $event); + if (is_string($val = $handler->getReturnVal()) || ($val instanceof Stringable)) { + $event->withResponse(HttpFactory::getInstance()->createResponse(200, null, [], Stream::create($val))); + } elseif ($event->getResponse() === null) { + $event->withResponse(HttpFactory::getInstance()->createResponse(500)); + } + logger()->warning('Used ' . round((microtime(true) - $starttime) * 1000, 3) . ' ms'); + break; + case ZM_ERR_ROUTE_METHOD_NOT_ALLOWED: + $event->withResponse(HttpUtil::handleHttpCodePage(405)); + break; + } + } + } + + /** + * 遍历结束所有的如果还是没有响应,那么就找静态文件路由 + * + * @throws ConfigException + */ + public function onRequest1(HttpRequestEvent $event) + { + if ($event->getResponse() === null) { + $response = HttpUtil::handleStaticPage($event->getRequest()->getUri()->getPath()); + $event->withResponse($response); + } + container()->flush(); } } diff --git a/src/ZM/Event/Listener/ManagerEventListener.php b/src/ZM/Event/Listener/ManagerEventListener.php index b74957dd..35b27afb 100644 --- a/src/ZM/Event/Listener/ManagerEventListener.php +++ b/src/ZM/Event/Listener/ManagerEventListener.php @@ -5,25 +5,36 @@ declare(strict_types=1); namespace ZM\Event\Listener; use OneBot\Util\Singleton; +use ZM\Exception\ZMKnownException; use ZM\Process\ProcessStateManager; class ManagerEventListener { use Singleton; + /** + * Manager 进程启动的回调(仅 Swoole 驱动才会回调) + */ public function onManagerStart() { // 自注册一下,刷新当前进程的logger进程banner ob_logger_register(ob_logger()); logger()->debug('Manager process started'); + // 注册 Manager 进程的信号 SignalListener::getInstance()->signalManager(); /* @noinspection PhpComposerExtensionStubsInspection */ ProcessStateManager::saveProcessState(ZM_PROCESS_MANAGER, posix_getpid()); } + /** + * Manager 进程停止的回调(仅 Swoole 驱动才会回调) + * @throws ZMKnownException + */ public function onManagerStop() { + logger()->debug('Manager process stopped'); + ProcessStateManager::removeProcessState(ZM_PROCESS_MANAGER); } } diff --git a/src/ZM/Event/Listener/WorkerEventListener.php b/src/ZM/Event/Listener/WorkerEventListener.php index e9b7a8de..a4310d99 100644 --- a/src/ZM/Event/Listener/WorkerEventListener.php +++ b/src/ZM/Event/Listener/WorkerEventListener.php @@ -6,23 +6,38 @@ namespace ZM\Event\Listener; use OneBot\Driver\Process\ProcessManager; use OneBot\Util\Singleton; +use Throwable; +use ZM\Annotation\AnnotationHandler; +use ZM\Annotation\AnnotationMap; +use ZM\Annotation\AnnotationParser; +use ZM\Annotation\Framework\Init; +use ZM\Container\ContainerServicesProvider; +use ZM\Exception\ZMKnownException; use ZM\Framework; use ZM\Process\ProcessStateManager; +use ZM\Utils\ZMUtil; class WorkerEventListener { use Singleton; + /** + * Driver 的 Worker 进程启动后执行的事件 + * + * @throws Throwable + */ public function onWorkerStart() { // 自注册一下,刷新当前进程的logger进程banner ob_logger_register(ob_logger()); + // 如果没有引入参数disable-safe-exit,则监听 Ctrl+C if (!Framework::getInstance()->getArgv()['disable-safe-exit'] && PHP_OS_FAMILY !== 'Windows') { SignalListener::getInstance()->signalWorker(); } logger()->debug('Worker #' . ProcessManager::getProcessId() . ' started'); + // 设置 Worker 进程的状态和 ID 等信息 if (($name = Framework::getInstance()->getDriver()->getName()) === 'swoole') { /* @phpstan-ignore-next-line */ $server = Framework::getInstance()->getDriver()->getSwooleServer(); @@ -30,11 +45,81 @@ class WorkerEventListener } elseif ($name === 'workerman' && DIRECTORY_SEPARATOR !== '\\' && extension_loaded('posix')) { ProcessStateManager::saveProcessState(ZM_PROCESS_WORKER, posix_getpid(), ['worker_id' => ProcessManager::getProcessId()]); } + + // 设置容器,注册容器提供商 + resolve(ContainerServicesProvider::class)->registerServices('global'); + + // 注册 Worker 进程遇到退出时的回调,安全退出 + register_shutdown_function(function () { + $error = error_get_last(); + // 下面这段代码的作用就是,不是错误引发的退出时照常退出即可 + if (($error['type'] ?? 0) != 0) { + logger()->emergency(zm_internal_errcode('E00027') . 'Internal fatal error: ' . $error['message'] . ' at ' . $error['file'] . "({$error['line']})"); + } elseif (!isset($error['type'])) { + return; + } + Framework::getInstance()->stop(); + }); + + // TODO: 注册各种池子 + + // 加载用户代码资源 + $this->loadUserSources(); + + // handle @Init annotation + $this->handleInit(); + + // 回显 debug 日志:进程占用的内存 + $memory_total = memory_get_usage() / 1024 / 1024; + logger()->debug('Worker process used ' . round($memory_total, 3) . ' MB'); } + /** + * @throws ZMKnownException + */ public function onWorkerStop() { logger()->debug('Worker #' . ProcessManager::getProcessId() . ' stopping'); ProcessStateManager::removeProcessState(ZM_PROCESS_WORKER, ProcessManager::getProcessId()); } + + /** + * 加载用户代码资源,包括普通插件、单文件插件、Composer 插件等 + * @throws Throwable + */ + private function loadUserSources() + { + logger()->debug('Loading user sources'); + + // 首先先加载 source 普通插件,相当于内部模块,不算插件的一种 + $parser = new AnnotationParser(); + $composer = ZMUtil::getComposerMetadata(); + // 合并 dev 和 非 dev 的 psr-4 加载目录 + $merge_psr4 = array_merge($composer['autoload']['psr-4'] ?? [], $composer['autoload-dev']['psr-4'] ?? []); + // 排除 composer.json 中指定需要排除的目录 + $excludes = $composer['extra']['zm']['exclude-annotation-path'] ?? []; + foreach ($merge_psr4 as $k => $v) { + // 如果在排除表就排除,否则就解析注解 + if (is_dir(SOURCE_ROOT_DIR . '/' . $v) && !in_array($v, $excludes)) { + // 添加解析路径,对应Base命名空间也贴出来 + $parser->addRegisterPath(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\')); + } + } + + // TODO: 然后加载插件目录下的插件 + + // 解析所有注册路径的文件,获取注解 + $parser->parseAll(); + // 将Parser解析后的注解注册到全局的 AnnotationMap + AnnotationMap::loadAnnotationByParser($parser); + } + + private function handleInit() + { + $handler = new AnnotationHandler(Init::class); + $handler->setRuleCallback(function (Init $anno) { + return $anno->worker === -1 || $anno->worker === ProcessManager::getProcessId(); + }); + $handler->handleAll(); + } } diff --git a/src/ZM/Exception/InvalidArgumentException.php b/src/ZM/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..76c95a0b --- /dev/null +++ b/src/ZM/Exception/InvalidArgumentException.php @@ -0,0 +1,15 @@ + $argv 传入的参数(见 ServerStartCommand) * @throws InitException - * @throws ConfigException * @throws Exception */ public function __construct(array $argv = []) @@ -73,7 +72,13 @@ class Framework // 初始化必需的args参数,如果没有传入的话,使用默认值 $this->argv = empty($argv) ? ServerStartCommand::exportOptionArray() : $argv; + } + /** + * @throws Exception + */ + public function init(): Framework + { // 执行一些 Driver 前置条件的内容 $this->initDriverPrerequisites(); @@ -82,6 +87,8 @@ class Framework // 初始化框架的交互以及框架部分自己要监听的事件 $this->initFramework(); + + return $this; } /** @@ -166,7 +173,7 @@ class Framework * * @throws ConfigException */ - private function initDriverPrerequisites() + public function initDriverPrerequisites() { // 寻找配置文件目录 if ($this->argv['config-dir'] !== null) { // 如果启动参数指定了config寻找目录,那么就在指定的寻找,不在别的地方寻找了 @@ -178,7 +185,7 @@ class Framework foreach ($find_dir as $v) { if (is_dir($v)) { ZMConfig::setDirectory($v); - ZMConfig::setEnv($this->argv['env'] ?? 'development'); + ZMConfig::setEnv($this->argv['env'] = $this->argv['env'] ?? 'development'); $config_done = true; break; } @@ -237,7 +244,7 @@ class Framework * * @throws Exception */ - private function initDriver() + public function initDriver() { switch ($driver = ZMConfig::get('global.driver')) { case 'swoole': @@ -263,7 +270,7 @@ class Framework * * @throws ConfigException */ - private function initFramework() + public function initFramework() { // private-mode 模式下,不输出任何内容 if (!$this->argv['private-mode']) { @@ -276,7 +283,16 @@ class Framework ob_event_provider()->addEventListener(WorkerStartEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStart'], 999); ob_event_provider()->addEventListener(WorkerStopEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStop'], 999); // Http 事件 - ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest'], 999); + ob_event_provider()->addEventListener(HttpRequestEvent::getName(), function () { + global $starttime; + $starttime = microtime(true); + }, 1000); + ob_event_provider()->addEventListener(HttpRequestEvent::getName(), function () { + global $starttime; + logger()->error('Finally used ' . round((microtime(true) - $starttime) * 1000, 4) . ' ms'); + }, 0); + ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest999'], 999); + ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest1'], 1); // manager 事件 ob_event_provider()->addEventListener(ManagerStartEvent::getName(), [ManagerEventListener::getInstance(), 'onManagerStart'], 999); ob_event_provider()->addEventListener(ManagerStopEvent::getName(), [ManagerEventListener::getInstance(), 'onManagerStop'], 999); @@ -300,7 +316,7 @@ class Framework // 打印工作目录 $properties['working_dir'] = WORKING_DIR; // 打印环境信息 - $properties['environment'] = ($this->argv['env'] ?? null) === null ? 'default' : $this->argv['env']; + $properties['environment'] = $this->argv['env']; // 打印驱动 $properties['driver'] = ZMConfig::get('global.driver'); // 打印logger显示等级 @@ -476,7 +492,6 @@ class Framework { if (Phar::running() !== '') { // 在 Phar 下,不需要新启动进程了,因为 Phar 没办法重载,自然不需要考虑多进程的加载 reload 问题 - /** @noinspection PhpIncludeInspection */ require FRAMEWORK_ROOT_DIR . '/src/Globals/script_setup_loader.php'; $r = _zm_setup_loader(); $result_code = 0; diff --git a/src/ZM/InstantApplication.php b/src/ZM/InstantApplication.php new file mode 100644 index 00000000..02b5bda1 --- /dev/null +++ b/src/ZM/InstantApplication.php @@ -0,0 +1,35 @@ +init()->start(); + } +} diff --git a/src/ZM/Middleware/MiddlewareHandler.php b/src/ZM/Middleware/MiddlewareHandler.php new file mode 100644 index 00000000..b63867c6 --- /dev/null +++ b/src/ZM/Middleware/MiddlewareHandler.php @@ -0,0 +1,204 @@ +middlewares[$name]['before'] = $callback; + } + + public function registerAfter(string $name, callable $callback) + { + if ( + is_array($callback) // 如果是数组类型callback + && is_object($callback[0]) // 且为动态调用 + && isset($this->middlewares[$name]['before']) // 且存在before + && is_array($this->middlewares[$name]['before']) // 且before也是数组类型callback + && is_object($this->middlewares[$name]['before'][0]) // 且before类型也为动态调用 + && get_class($this->middlewares[$name]['before'][0]) === get_class($callback[0]) // 且before和after在一个类 + ) { + // 那么就把after的对象替换为和before同一个 + $callback[0] = $this->middlewares[$name]['before'][0]; + } + $this->middlewares[$name]['after'] = $callback; + } + + public function registerException(string $name, string $exception_class, callable $callback) + { + $this->middlewares[$name]['exception'][$exception_class] = $callback; + } + + /** + * @throws InvalidArgumentException + */ + public function bindMiddleware(callable $callback, string $name, array $params = []) + { + $stack_id = $this->getStackId($callback); + // TODO: 对中间件是否存在进行检查 + if (class_exists($name)) { + $obj = resolve($name); + } + + $this->reg_map[$stack_id][] = [$name, $params]; + } + + /** + * @throws InvalidArgumentException + * @throws Throwable + */ + public function process(callable $callback, array $args) + { + try { + $before_result = MiddlewareHandler::getInstance()->processBefore($callback, $args); + if ($before_result) { + $result = container()->call($callback, $args); + } + MiddlewareHandler::getInstance()->processAfter($callback, $args); + } catch (Throwable $e) { + MiddlewareHandler::getInstance()->processException($callback, $args, $e); + } + return $result ?? null; + } + + /** + * 调用中间件的前 + * + * @param callable $callback 必须是数组形式的动态调用 + * @param array $args 参数列表 + * @throws InvalidArgumentException + */ + public function processBefore(callable $callback, array $args): bool + { + // 压栈ID + $stack_id = $this->getStackId($callback); + // 清除之前的 + unset($this->stack[$stack_id]); + $this->callable_stack[] = $callback; + // 遍历执行before并压栈,并在遇到返回false后停止 + try { + foreach (($this->reg_map[$stack_id] ?? []) as $item) { + $this->stack[$stack_id][] = $item; + if (isset($this->middlewares[$item[0]]['before'])) { + $return = container()->call($this->middlewares[$item[0]]['before'], $args); + if ($return === false) { + array_pop($this->callable_stack); + return false; + } + } + } + } finally { + array_pop($this->callable_stack); + } + return true; + } + + /** + * 获取正在运行的回调调用对象,可能是Closure、array、string + * + * @return false|mixed + */ + public function getCurrentCallable() + { + return end($this->callable_stack); + } + + /** + * TODO: 调用中间件的后 + * + * @param callable $callback 必须是数组形式的动态调用 + * @param array $args 参数列表 + * @throws InvalidArgumentException + */ + public function processAfter(callable $callback, array $args) + { + // 压栈ID + $stack_id = $this->getStackId($callback); + // 从栈内倒序取出已经执行过的中间件,并执行after + $this->callable_stack[] = $callback; + try { + while (isset($this->stack[$stack_id]) && ($item = array_pop($this->stack[$stack_id])) !== null) { + if (isset($this->middlewares[$item[0]]['after'])) { + container()->call($this->middlewares[$item[0]]['after'], $args); + } + } + } finally { + array_pop($this->callable_stack); + } + } + + /** + * TODO: 调用中间件的异常捕获处理 + * + * @param callable $callback 必须是数组形式的动态调用 + * @param array $args 参数列表 + * @throws InvalidArgumentException + * @throws Throwable + */ + public function processException(callable $callback, array $args, Throwable $throwable) + { + // 压栈ID + $stack_id = $this->getStackId($callback); + // 从栈内倒序取出已经执行过的中间件,并执行after + while (isset($this->stack[$stack_id]) && ($item = array_pop($this->stack[$stack_id])) !== null) { + foreach ($this->middlewares[$item[0]]['exception'] as $k => $v) { + if (is_a($throwable, $k)) { + $v($throwable, ...$args); + unset($this->stack[$stack_id]); + return; + } + } + } + throw $throwable; + } + + /** + * @param callable $callback 可执行的方法 + * @throws InvalidArgumentException + */ + private function getStackId(callable $callback): string + { + if ($callback instanceof Closure) { + // 闭包情况下,直接根据闭包的ID号来找stack + return strval(spl_object_id($callback)); + } + if (is_array($callback) && count($callback) === 2) { + // 活性调用,根据组合名称来判断 + return (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1]; + } + if (is_string($callback)) { + return $callback; + } + throw new InvalidArgumentException('传入的 callable 有误!'); + } +} diff --git a/src/ZM/Middleware/MiddlewareInterface.php b/src/ZM/Middleware/MiddlewareInterface.php new file mode 100644 index 00000000..c3701793 --- /dev/null +++ b/src/ZM/Middleware/MiddlewareInterface.php @@ -0,0 +1,9 @@ +registerBefore(static::class, [$this, 'onBefore']); + middleware()->registerAfter(static::class, [$this, 'onAfter']); + } + + public function onBefore(): bool + { + $this->starttime = microtime(true); + return true; + } + + public function onAfter() + { + logger()->info('Using ' . round((microtime(true) - $this->starttime) * 1000, 4) . ' ms'); + } +} diff --git a/src/ZM/Plugin/InstantPlugin.php b/src/ZM/Plugin/InstantPlugin.php new file mode 100644 index 00000000..9421e2b6 --- /dev/null +++ b/src/ZM/Plugin/InstantPlugin.php @@ -0,0 +1,77 @@ +dir = $dir; + } + + public function getDir(): string + { + return $this->dir; + } + + public function addBotEvent(BotEvent $event) + { + $this->bot_events[] = $event; + } + + public function addBotCommand(BotCommand $command) + { + $this->bot_commands[] = $command; + } + + public function registerEvent(string $event_name, callable $callback, int $level = 20) + { + $this->events[] = [$event_name, $callback, $level]; + } + + public function addHttpRoute(Route $route) + { + $this->routes[] = $route; + } + + public function getBotEvents(): array + { + return $this->bot_events; + } + + public function getBotCommands(): array + { + return $this->bot_commands; + } + + public function getEvents(): array + { + return $this->events; + } + + public function getRoutes(): array + { + return $this->routes; + } +} diff --git a/src/ZM/Store/FileSystem.php b/src/ZM/Store/FileSystem.php index 98f5aa3d..bedf3a66 100644 --- a/src/ZM/Store/FileSystem.php +++ b/src/ZM/Store/FileSystem.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace ZM\Store; use RuntimeException; +use ZM\Utils\ZMUtil; class FileSystem { @@ -113,7 +114,7 @@ class FileSystem public static function getClassesPsr4(string $dir, string $base_namespace, $rule = null, $return_path_value = false): array { // 预先读取下composer的file列表 - $composer = json_decode(file_get_contents(zm_dir(SOURCE_ROOT_DIR . '/composer.json')), true); + $composer = ZMUtil::getComposerMetadata(); $classes = []; // 扫描目录,使用递归模式,相对路径模式,因为下面此路径要用作转换成namespace $files = FileSystem::scanDirFiles($dir, true, true); @@ -142,7 +143,7 @@ class FileSystem /*if (substr(file_get_contents($dir . '/' . $v), 6, 6) == '#plain') { continue; }*/ - if (file_exists($dir . '/' . $pathinfo['basename'] . '.plain')) { + if (file_exists($dir . '/' . $pathinfo['basename'] . '.ignore')) { continue; } if (mb_substr($pathinfo['basename'], 0, 7) == 'global_' || mb_substr($pathinfo['basename'], 0, 7) == 'script_') { diff --git a/src/ZM/Store/InternalGlobals.php b/src/ZM/Store/InternalGlobals.php deleted file mode 100644 index c4adb738..00000000 --- a/src/ZM/Store/InternalGlobals.php +++ /dev/null @@ -1,19 +0,0 @@ -setMethod($request->getMethod()); + + try { + // 使用UrlMatcher进行匹配Url + $matcher = new UrlMatcher(static::getRouteCollection(), $context); + $matched = $matcher->match($request->getUri()->getPath()); + } catch (ResourceNotFoundException $e) { + // 路由找不到会抛出异常,我们不需要这个异常,转换为状态码 + return ZM_ERR_ROUTE_NOT_FOUND; + } catch (MethodNotAllowedException $e) { + // 路由匹配到了,但该路由不能使用该方法,所以返回状态码(路由不允许) + return ZM_ERR_ROUTE_METHOD_NOT_ALLOWED; + } + // 匹配到的时候,matched不为空 + if (!empty($matched)) { + $node = [ + 'route' => static::getRouteCollection()->get($matched['_route'])->getPath(), + 'class' => $matched['_class'], + 'method' => $matched['_method'], + 'request_method' => $request->getMethod(), + ]; + unset($matched['_class'], $matched['_method']); + $params = $matched; + // 返回成功的状态码 + return ZM_ERR_NONE; + } + // 返回没有匹配到的状态码 + return ZM_ERR_ROUTE_NOT_FOUND; + } + + /** + * 解析返回静态文件 + * + * @params string $uri 路由地址 + * @params string $settings 动态传入的配置模式 + * @throws ConfigException + */ + public static function handleStaticPage(string $uri, array $settings = []): ResponseInterface + { + // 确定根目录 + $base_dir = $settings['document_root'] ?? ZMConfig::get('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'); + if (is_string($base_index)) { + $base_index = [$base_index]; + } + $path = realpath($base_dir . urldecode($uri)); + if ($path !== false) { + // 安全问题,防止目录穿越,只能囚禁到规定的 Web 根目录下获取文件 + $work = realpath($base_dir) . '/'; + if (strpos($path, $work) !== 0) { + logger()->info('[403] ' . $uri); + return static::handleHttpCodePage(403); + } + // 如果路径是文件夹的话,如果结尾没有 /,则自动302补充,和传统的Nginx效果相同 + if (is_dir($path)) { + if (mb_substr($uri, -1, 1) != '/') { + logger()->info('[302] ' . $uri); + return HttpFactory::getInstance()->createResponse(302, null, ['Location' => $uri . '/']); + } + // 如果结尾有 /,那么就根据默认搜索的文件名进行搜索文件是否存在,存在则直接返回对应文件 + foreach ($base_index as $vp) { + if (is_file($path . '/' . $vp)) { + 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') + ->withBody(HttpFactory::getInstance()->createStream(file_get_contents($path . '/' . $vp))); + } + } + // 如果文件存在,则直接返回文件内容 + } elseif (is_file($path)) { + 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') + ->withBody(HttpFactory::getInstance()->createStream(file_get_contents($path))); + } + } + // 否则最终肯定只能返回 404 了 + logger()->info('[404] ' . $uri); + return static::handleHttpCodePage(404); + } + + /** + * 自动寻找默认的 HTTP Code 页面 + * + * @throws ConfigException + */ + 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 = 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)); + } + + /** + * 快速创建一个 JSON 格式的 HTTP 响应 + * + * @param array $data 数据 + * @param int $http_code HTTP 状态码 + * @param int $json_flag JSON 编码时传入的flag + */ + public static function createJsonResponse(array $data, int $http_code = 200, int $json_flag = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE): ResponseInterface + { + return HttpFactory::getInstance()->createResponse($http_code) + ->withAddedHeader('Content-Type', 'application/json') + ->withBody(Stream::create(json_encode($data, $json_flag))); + } + + public static function getRouteCollection(): RouteCollection + { + if (self::$routes === null) { + self::$routes = new RouteCollection(); + } + return self::$routes; + } +} diff --git a/src/ZM/Utils/ReflectionUtil.php b/src/ZM/Utils/ReflectionUtil.php new file mode 100644 index 00000000..34a288a7 --- /dev/null +++ b/src/ZM/Utils/ReflectionUtil.php @@ -0,0 +1,123 @@ +getType(); + // 没有声明类型或为基本类型 + if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) { + return null; + } + + // 获取类名 + $class_name = $type->getName(); + + // 如果存在父类 + if (!is_null($class = $parameter->getDeclaringClass())) { + if ($class_name === 'self') { + return $class->getName(); + } + + if ($class_name === 'parent' && $parent = $class->getParentClass()) { + return $parent->getName(); + } + } + + return $class_name; + } + + /** + * 将传入变量转换为字符串 + * + * @param mixed $var + */ + public static function variableToString($var): string + { + switch (true) { + case is_callable($var): + if (is_array($var)) { + if (is_object($var[0])) { + return get_class($var[0]) . '@' . $var[1]; + } + return $var[0] . '::' . $var[1]; + } + return 'closure'; + case is_string($var): + return $var; + case is_array($var): + return 'array' . json_encode($var); + case is_object($var): + return get_class($var); + case is_resource($var): + return 'resource(' . get_resource_type($var) . ')'; + case is_null($var): + return 'null'; + case is_bool($var): + return $var ? 'true' : 'false'; + case is_float($var): + case is_int($var): + return (string) $var; + default: + return 'unknown'; + } + } + + /** + * 判断传入的回调是否为任意类的非静态方法 + * + * @param callable|string $callback 回调 + * @throws ReflectionException + */ + public static function isNonStaticMethod($callback): bool + { + if (is_array($callback) && is_string($callback[0])) { + $reflection = new ReflectionMethod($callback[0], $callback[1]); + return !$reflection->isStatic(); + } + return false; + } + + /** + * 获取传入的回调的反射实例 + * + * 如果传入的是类方法,则会返回 {@link ReflectionMethod} 实例 + * 否则将返回 {@link ReflectionFunction} 实例 + * + * 可传入实现了 __invoke 的类 + * + * @param callable|string $callback 回调 + * @throws ReflectionException + */ + public static function getCallReflector($callback): ReflectionFunctionAbstract + { + if (is_string($callback) && str_contains($callback, '::')) { + $callback = explode('::', $callback); + } elseif (is_object($callback) && !$callback instanceof Closure) { + $callback = [$callback, '__invoke']; + } + + return is_array($callback) + ? new ReflectionMethod($callback[0], $callback[1]) + : new ReflectionFunction($callback); + } +} diff --git a/src/ZM/Utils/ZMUtil.php b/src/ZM/Utils/ZMUtil.php index f53d33a5..b27c7be4 100644 --- a/src/ZM/Utils/ZMUtil.php +++ b/src/ZM/Utils/ZMUtil.php @@ -6,4 +6,11 @@ namespace ZM\Utils; class ZMUtil { + /** + * 获取 composer.json 并转为数组进行读取使用 + */ + public static function getComposerMetadata(): ?array + { + return json_decode(file_get_contents(SOURCE_ROOT_DIR . '/composer.json'), true); + } } diff --git a/src/entry.php b/src/entry.php index d8d57665..754cf405 100644 --- a/src/entry.php +++ b/src/entry.php @@ -2,7 +2,13 @@ declare(strict_types=1); -// CLI Application 入口文件,先引入 Composer 组件 +use OneBot\Driver\ExceptionHandler; + +/** + * CLI Application 入口文件,先引入 Composer 组件 + * + * @noinspection PhpIncludeInspection + */ require_once((!is_dir(__DIR__ . '/../vendor')) ? getcwd() : (__DIR__ . '/..')) . '/vendor/autoload.php'; // 适配 Windows 的 conhost 中文显示,因为使用 micro 打包框架运行的时候在 Windows 运行中文部分会变成乱码 @@ -11,4 +17,9 @@ if (DIRECTORY_SEPARATOR === '\\') { } // 开始运行,运行 symfony console 组件并解析命令 -(new ZM\ConsoleApplication('zhamao-framework'))->run(); +try { + (new ZM\ConsoleApplication('zhamao-framework'))->run(); +} catch (Exception $e) { + ExceptionHandler::getInstance()->handle($e); + exit(1); +} diff --git a/tests/ZM/Utils/HttpUtilTest.php b/tests/ZM/Utils/HttpUtilTest.php index 459e660a..44177dc0 100644 --- a/tests/ZM/Utils/HttpUtilTest.php +++ b/tests/ZM/Utils/HttpUtilTest.php @@ -6,11 +6,9 @@ namespace Tests\ZM\Utils; use PHPUnit\Framework\TestCase; use Swoole\Http\Request; -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; @@ -19,17 +17,6 @@ use ZM\Utils\Manager\RouteManager; */ class HttpUtilTest extends TestCase { - /** - * @dataProvider providerTestHandleStaticPage - */ - public function testHandleStaticPage(string $page, bool $expected): void - { - $swoole_response = $this->getMockClass(Response::class); - $r = new \ZM\Http\Response(new $swoole_response()); - HttpUtil::handleStaticPage($page, $r, ZMConfig::get('global', 'static_file_server')); - $this->assertEquals($expected, $r->getStatusCode() === 200); - } - public function providerTestHandleStaticPage(): array { return [ @@ -38,17 +25,6 @@ class HttpUtilTest extends TestCase ]; } - /** - * @covers \ZM\Utils\HttpUtil::getHttpCodePage - * @covers \ZM\Utils\HttpUtil::responseCodePage - * @dataProvider providerTestGetHttpCodePage - */ - public function testGetHttpCodePage(int $code, bool $expected): void - { - $has_response = !empty(HttpUtil::getHttpCodePage($code)); - $this->assertSame($expected, $has_response); - } - public function providerTestGetHttpCodePage(): array { return [