diff --git a/composer.json b/composer.json index ea84a726..0f0361f5 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,8 @@ }, "bin": [ "bin/phpunit-zm", - "bin/zhamao" + "bin/zhamao", + "bin/zhamao.bat" ], "config": { "allow-plugins": { diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 4d015dc9..83b6ea29 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -50,6 +50,39 @@ composer require zhamao/framework 如果你打算在 Windows 使用原生的 Win 环境 PHP,你需要先安装 PHP 和 Composer,然后在任意目录下执行上方 composer 的安装方法即可。 +### 包管理安装 + +Windows 也可以使用包管理安装 PHP、Composer,例如你可以使用 Scoop 包管理进行安装: + +```powershell +scoop install php +scoop install composer +``` + +采用这种包管理安装后,可直接使用 `php`、`composer` 命令在任意位置,无需配置环境变量。 + +如果你使用包管理或已经安装了 PHP 到系统内,接下来就直接使用 Composer 来安装框架即可! + +```powershell +composer create-project zhamao/framework-starter zhamao-v3 +cd zhamao-v3 +./zhamao plugin:make +./zhamao server +``` + +### 纯手动安装 + +如果你不想使用包管理的方式安装 PHP,且让 PHP 仅框架独立使用,你可以依次采用以下的方式来安装 PHP、Composer 和框架: + +1. 从 GitHub 下载框架的脚手架,地址: +2. 解压框架脚手架,重命名文件夹名称为你自己喜欢的名称,例如 `zhamao-v3`。 +3. 从 PHP 官网下载 PHP,选择 `Non Thread Safe` 版本,PHP 版本选择 8.0 ~ 8.2 均可(推荐 8.1),下载完成后解压到框架目录下的 `runtime\php` 目录,例如 `D:\zhamao-v3\runtime\php\`。 +4. 从 [Composer 官网](https://getcomposer.org/download/) 或 [阿里云镜像](https://mirrors.aliyun.com/composer/composer.phar) 下载 Composer,下载到 `runtime\` 目录。 +5. 在你的脚手架目录下执行 `.\runtime\php\php.exe .\runtime\composer.phar install` 安装框架依赖。 +6. 执行框架初始化命令:`./zhamao init`。 +7. 接下来你就可以使用和上方所有框架操作指令相同的内容了,例如 `./zhamao plugin:make`、`./zhamao server` 等。 +8. 如果你需要使用 Composer,你可以使用 `.\runtime\php\php.exe .\runtime\composer.phar` 来代替 `composer` 命令。 + ## 更多的环境部署和开发方式 除了上述方式之外,框架还支持源码模式、守护进程等运行方式,详情请参阅 [进阶开发]。 diff --git a/ext/v3.sh b/ext/v3.sh index b8217d34..6ab45f53 100755 --- a/ext/v3.sh +++ b/ext/v3.sh @@ -156,7 +156,7 @@ function darwin_env_check() { # 询问是否安装 native php function prompt_install_native_php() { - echo -ne "$(nhead yellow) 检测到系统的 PHP 不存在 swoole 扩展,是否下载安装独立的内建 PHP 和 Composer?[Y/n] " + echo -ne "$(nhead yellow) 检测到系统的 PHP 不符合要求,是否下载安装独立的内建 PHP 和 Composer?[Y/n] " read -r y case $y in Y|y|"") return 0 ;; diff --git a/src/Globals/script_setup_loader.php b/src/Globals/script_setup_loader.php index a32d1111..c86d8ae3 100644 --- a/src/Globals/script_setup_loader.php +++ b/src/Globals/script_setup_loader.php @@ -21,7 +21,7 @@ function _zm_setup_loader() // 如果在排除表就排除,否则就解析注解 if (is_dir(SOURCE_ROOT_DIR . '/' . $v) && !in_array($v, $excludes)) { // 添加解析路径,对应Base命名空间也贴出来 - $parser->addRegisterPath(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\')); + $parser->addPsr4Path(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\')); } } $parser->addSpecialParser(Setup::class, function (Setup $setup) { @@ -36,7 +36,7 @@ function _zm_setup_loader() // TODO: 然后加载插件目录下的插件 // 解析所有注册路径的文件,获取注解 - $parser->parseAll(); + $parser->parse(); return json_encode(['setup' => $_tmp_setup_list]); } catch (Throwable $e) { diff --git a/src/ZM/Annotation/AnnotationMap.php b/src/ZM/Annotation/AnnotationMap.php index 114ff7e7..2f883b38 100644 --- a/src/ZM/Annotation/AnnotationMap.php +++ b/src/ZM/Annotation/AnnotationMap.php @@ -27,16 +27,22 @@ class AnnotationMap */ public static array $_map = []; - /** - * 将Parser解析后的注解注册到全局的 AnnotationMap - * - * @param AnnotationParser $parser 注解解析器 - */ - public static function loadAnnotationByParser(AnnotationParser $parser): void + public static function loadAnnotationList(array $list): void { - // 生成后加入到全局list中 - self::$_list = array_merge_recursive(self::$_list, $parser->generateAnnotationList()); - self::$_map = $parser->getAnnotationMap(); + self::$_list = array_merge_recursive(self::$_list, $list); + } + + public static function loadAnnotationMap(array $map): void + { + self::$_map = array_merge_recursive(self::$_map, $map); + } + + /** + * @return AnnotationBase[] + */ + public static function getAnnotationList(string $class_name): array + { + return self::$_list[$class_name] ?? []; } /** diff --git a/src/ZM/Annotation/AnnotationParser.php b/src/ZM/Annotation/AnnotationParser.php index f9c4dffc..e4943e58 100644 --- a/src/ZM/Annotation/AnnotationParser.php +++ b/src/ZM/Annotation/AnnotationParser.php @@ -20,9 +20,9 @@ use ZM\Utils\HttpUtil; class AnnotationParser { /** - * @var array 要解析的路径列表 + * @var array 要解析的 PSR-4 class 列表 */ - private array $path_list = []; + private array $class_list = []; /** * @var float 用于计算解析时间用的 @@ -56,6 +56,7 @@ class AnnotationParser $this->special_parsers = [ Middleware::class => [function (Middleware $middleware) { \middleware()->bindMiddleware([resolve($middleware->class), $middleware->method], $middleware->name, $middleware->params); }], Route::class => [[$this, 'addRouteAnnotation']], + Closed::class => [function () { return false; }], ]; } } @@ -71,13 +72,21 @@ class AnnotationParser $this->special_parsers[$class_name][] = $callback; } - public function parse(array $path): void + /** + * 解析所有传入的 PSR-4 下识别出来的类及下方的注解 + * 返回一个包含三个元素的数组,分别是list、map、tree + * 其中list为注解列表,key是注解的class名称,value是所有此注解的列表,即[Annotation1, ...] + * map是类、方法映射表关系的三维数组,即[类名 => [方法名 => [注解1, ...]]] + * tree是解析中间生成的树结构,内含反射对象,见下方注释 + * + * @return array[] + * @throws \ReflectionException + */ + public function parse(): array { - // 写日志 - logger()->debug('parsing annotation in ' . $path[0] . ':' . $path[1]); - - // 首先获取路径下所有的类(通过 PSR-4 标准解析) - $all_class = FileSystem::getClassesPsr4($path[0], $path[1]); + $reflection_tree = []; + $annotation_map = []; + $annotation_list = []; // 读取配置文件中配置的忽略解析的注解名,防止误解析一些别的地方需要的注解,比如@mixin $conf = config('global.runtime.annotation_reader_ignore'); @@ -97,7 +106,7 @@ class AnnotationParser // 声明一个既可以解析注解又可以解析Attribute的双reader来读取注解和Attribute $reader = new DualReader(new AnnotationReader(), new AttributeReader()); - foreach ($all_class as $v) { + foreach ($this->class_list as $v) { logger()->debug('正在检索 ' . $v); // 通过反射实现注解读取 @@ -107,7 +116,7 @@ class AnnotationParser // 这段为新加的:start // 这里将每个类里面所有的类注解、方法注解通通加到一颗大树上,后期解析 /* - $annotation_map: { + $reflection_tree: { Module\Example\Hello: { class_annotations: [ 注解对象1, 注解对象2, ... @@ -124,16 +133,16 @@ class AnnotationParser */ // 保存对class的注解 - $this->annotation_tree[$v]['class_annotations'] = $class_annotations; + $reflection_tree[$v]['class_annotations'] = $class_annotations; // 保存类成员的方法的对应反射对象们 - $this->annotation_tree[$v]['methods'] = $methods; + $reflection_tree[$v]['methods'] = $methods; // 保存对每个方法获取到的注解们 foreach ($methods as $method) { - $this->annotation_tree[$v]['methods_annotations'][$method->getName()] = $reader->getMethodAnnotations($method); + $reflection_tree[$v]['methods_annotations'][$method->getName()] = $reader->getMethodAnnotations($method); } // 因为适用于类的注解有一些比较特殊,比如有向下注入的,有控制行为的,所以需要遍历一下下放到方法里 - foreach ($this->annotation_tree[$v]['class_annotations'] as $vs) { + foreach ($reflection_tree[$v]['class_annotations'] as $vs) { $vs->class = $v; // 预处理0:排除所有非继承于 AnnotationBase 的注解 @@ -142,33 +151,30 @@ class AnnotationParser 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) { + foreach (($reflection_tree[$v]['methods'] ?? []) as $method) { // 用 clone 的目的是生成个独立的对象,避免和 class 以及方法之间互相冲突 $copy = clone $vs; $copy->method = $method->getName(); - $this->annotation_tree[$v]['methods_annotations'][$method->getName()][] = $copy; + $reflection_tree[$v]['methods_annotations'][$method->getName()][] = $copy; + $annotation_list[get_class($vs)][] = $copy; } } // 预处理3:调用自定义解析器 - if (($a = $this->parseSpecial($vs)) === true) { + if (($a = $this->parseSpecial($vs, $reflection_tree[$v]['class_annotations'])) === true) { continue; } if ($a === false) { + unset($reflection_tree[$v]); continue 2; } + $annotation_list[get_class($vs)][] = $vs; } // 预处理3:处理每个函数上面的特殊注解,就是需要操作一些东西的 - foreach (($this->annotation_tree[$v]['methods_annotations'] ?? []) as $method_name => $methods_annotations) { + foreach (($reflection_tree[$v]['methods_annotations'] ?? []) as $method_name => $methods_annotations) { foreach ($methods_annotations as $method_anno) { // 预处理3.0:排除所有非继承于 AnnotationBase 的注解 if (!$method_anno instanceof AnnotationBase) { @@ -180,43 +186,31 @@ class AnnotationParser $method_anno->class = $v; $method_anno->method = $method_name; - // 预处理3.2:如果包含了@Closed注解,则跳过这个方法的注解解析 - if ($method_anno instanceof Closed) { - unset($this->annotation_tree[$v]['methods_annotations'][$method_name]); - continue 2; - } - // 预处理3.3:调用自定义解析器 - if (($a = $this->parseSpecial($method_anno, $methods_annotations)) === true) { + $a = $this->parseSpecial($method_anno, $methods_annotations); + + if ($a === true) { continue; } if ($a === false) { + unset($reflection_tree[$v]['methods_annotations'][$method_name]); continue 2; } - // 如果上方没有解析或返回了 true,则添加到注解解析列表中 - $this->annotation_map[$v][$method_name][] = $method_anno; + $annotation_map[$v][$method_name][] = $method_anno; + $annotation_list[get_class($method_anno)][] = $method_anno; } } } - } - - /** - * 注册各个模块类的注解和模块level的排序 - */ - public function parseAll(): void - { - // 对每个设置的路径依次解析 - foreach ($this->path_list as $path) { - $this->parse($path); - } logger()->debug('解析注解完毕!'); + // ob_dump($annotation_list); + return [$annotation_list, $annotation_map, $reflection_tree]; } /** * 生成排序后的注解列表 */ - public function generateAnnotationList(): array + public function generateAnnotationListFromMap(): array { $o = []; foreach ($this->annotation_tree as $obj) { @@ -253,10 +247,11 @@ class AnnotationParser * @param string $path 注册解析注解的路径 * @param string $indoor_name 起始命名空间的名称 */ - public function addRegisterPath(string $path, string $indoor_name) + public function addPsr4Path(string $path, string $indoor_name) { logger()->debug('Add register path: ' . $path . ' => ' . $indoor_name); - $this->path_list[] = [$path, $indoor_name]; + $all_class = FileSystem::getClassesPsr4($path, $indoor_name); + $this->class_list = array_merge($this->class_list, $all_class); } /** diff --git a/src/ZM/Annotation/Framework/Init.php b/src/ZM/Annotation/Framework/Init.php index edda9562..ad4c8296 100644 --- a/src/ZM/Annotation/Framework/Init.php +++ b/src/ZM/Annotation/Framework/Init.php @@ -8,6 +8,7 @@ use Attribute; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Doctrine\Common\Annotations\Annotation\Target; use ZM\Annotation\AnnotationBase; +use ZM\Annotation\Interfaces\Level; /** * Class Init @@ -17,9 +18,19 @@ use ZM\Annotation\AnnotationBase; * @since 3.0.0 */ #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)] -class Init extends AnnotationBase +class Init extends AnnotationBase implements Level { - public function __construct(public int $worker = 0) + public function __construct(public int $worker = 0, public int $level = 20) { } + + public function getLevel() + { + return $this->level; + } + + public function setLevel($level) + { + $this->level = $level; + } } diff --git a/src/ZM/Event/Listener/WorkerEventListener.php b/src/ZM/Event/Listener/WorkerEventListener.php index 6dc8d705..ae4cf423 100644 --- a/src/ZM/Event/Listener/WorkerEventListener.php +++ b/src/ZM/Event/Listener/WorkerEventListener.php @@ -151,7 +151,7 @@ class WorkerEventListener // 如果在排除表就排除,否则就解析注解 if (is_dir(SOURCE_ROOT_DIR . '/' . $v) && !in_array($v, $excludes)) { // 添加解析路径,对应Base命名空间也贴出来 - $parser->addRegisterPath(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\')); + $parser->addPsr4Path(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\')); } } @@ -162,9 +162,9 @@ class WorkerEventListener continue; } match ($name) { - 'onebot12' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new OneBot12Adapter(parser: $parser)]), - 'onebot12-ban-other-ws' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new OneBot12Adapter(submodule: $name)]), - 'command-manual' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new CommandManualPlugin($parser)]), + 'onebot12' => PluginManager::addPlugin(['name' => $name, 'version' => '1.0', 'internal' => true, 'object' => new OneBot12Adapter(parser: $parser)]), + 'onebot12-ban-other-ws' => PluginManager::addPlugin(['name' => $name, 'version' => '1.0', 'internal' => true, 'object' => new OneBot12Adapter(submodule: $name)]), + 'command-manual' => PluginManager::addPlugin(['name' => $name, 'version' => '1.0', 'internal' => true, 'object' => new CommandManualPlugin($parser)]), }; } @@ -186,9 +186,10 @@ class WorkerEventListener } // 解析所有注册路径的文件,获取注解 - $parser->parseAll(); + [$list, $map] = $parser->parse(); // 将Parser解析后的注解注册到全局的 AnnotationMap - AnnotationMap::loadAnnotationByParser($parser); + AnnotationMap::loadAnnotationList($list); + AnnotationMap::loadAnnotationMap($map); // 排序所有的 AnnotationMap::sortAnnotationList(); } diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index 5c113602..c39ef11f 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -46,7 +46,7 @@ class Framework public const VERSION_ID = 659; /** @var string 版本名称 */ - public const VERSION = '3.0.0-beta4'; + public const VERSION = '3.0.0-beta5'; /** @var array 传入的参数 */ protected array $argv; diff --git a/src/ZM/Plugin/OneBot12Adapter.php b/src/ZM/Plugin/OneBot12Adapter.php index 5f91d67c..578be6a4 100644 --- a/src/ZM/Plugin/OneBot12Adapter.php +++ b/src/ZM/Plugin/OneBot12Adapter.php @@ -58,7 +58,7 @@ class OneBot12Adapter extends ZMPlugin // 处理和声明所有 BotCommand 下的 CommandArgument $parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']); // 不需要给列表写入 CommandArgument - $parser->addSpecialParser(CommandArgument::class, [$this, 'parseCommandArgument']); + $parser->addSpecialParser(CommandArgument::class, fn () => true); break; case 'onebot12-ban-other-ws': // 禁止其他类型的 WebSocket 客户端接入 @@ -86,14 +86,6 @@ class OneBot12Adapter extends ZMPlugin return null; } - /** - * 忽略解析记录 CommandArgument 注解 - */ - public function parseCommandArgument(): ?bool - { - return true; - } - /** * [CALLBACK] 调用 BotCommand 注解的方法 * diff --git a/src/ZM/Plugin/PluginManager.php b/src/ZM/Plugin/PluginManager.php index 3736a71d..eb9facbf 100644 --- a/src/ZM/Plugin/PluginManager.php +++ b/src/ZM/Plugin/PluginManager.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ZM\Plugin; +use Jelix\Version\VersionComparator; use ZM\Annotation\AnnotationMap; use ZM\Annotation\AnnotationParser; use ZM\Annotation\Framework\BindEvent; @@ -166,7 +167,8 @@ class PluginManager /** * 启用所有插件 * - * @param AnnotationParser $parser 传入注解解析器,用于将插件中的事件注解解析出来 + * @param AnnotationParser $parser 传入注解解析器,用于将插件中的事件注解解析出来 + * @throws PluginException */ public static function enablePlugins(AnnotationParser $parser): void { @@ -174,6 +176,15 @@ class PluginManager if (!isset($plugin['internal'])) { logger()->info('Enabling plugin: ' . $name); } + // 先判断下依赖关系,如果声明了依赖,但依赖不合规直接报错崩溃 + foreach (($plugin['dependencies'] ?? []) as $dep_name => $dep_version) { + if (!isset(self::$plugins[$dep_name])) { + throw new PluginException('插件 ' . $name . ' 依赖插件 ' . $dep_name . ',但是没有找到这个插件'); + } + if (VersionComparator::compareVersionRange(self::$plugins[$dep_name]['version'] ?? '1.0', $dep_version) === false) { + throw new PluginException('插件 ' . $name . ' 依赖插件 ' . $dep_name . ',但是这个插件的版本不符合要求'); + } + } if (isset($plugin['object']) && $plugin['object'] instanceof ZMPlugin) { $obj = $plugin['object']; // 将 Event 加入事件监听 @@ -197,7 +208,7 @@ class PluginManager } } elseif (isset($plugin['autoload'], $plugin['dir'])) { foreach ($plugin['autoload'] as $k => $v) { - $parser->addRegisterPath($plugin['dir'] . '/' . $v . '/', trim($k, '\\')); + $parser->addPsr4Path($plugin['dir'] . '/' . $v . '/', trim($k, '\\')); } } }