Compare commits

...

55 Commits
2.0.0 ... 2.2.0

Author SHA1 Message Date
jerry
484ddf9dfa update Hello.php
update docs
2021-01-29 21:30:19 +08:00
jerry
b611b4aad6 add DaemonCommand for daemon players
adjust http_header available
2021-01-29 20:47:00 +08:00
jerry
b9f973c718 add unset for WorkerCache.php 2021-01-20 18:43:22 +08:00
jerry
cd6c971547 update Singleton 2021-01-20 16:45:50 +08:00
jerry
c68083484a update to 2.2.0 version
add OnPipeMessageEvent.php
add ProcessManager.php
add WorkerCache component
fix route bug
correct Exception to ZMException
2021-01-20 16:11:04 +08:00
jerry
f999e689bf update docs 2021-01-18 18:09:33 +08:00
jerry
187a08a621 update to 2.1.6 version
优化代码结构
增加更多提示语
修复处理空格消息时的报错
修复上下文的bug
2021-01-18 18:08:29 +08:00
Whale
c208298937 Update README.md 2021-01-14 18:32:54 +08:00
crazywhalecc
e9e3e5e129 update docs 2021-01-13 15:46:55 +08:00
crazywhalecc
1ef8225d10 update to 2.1.5 version
change route to Symfony routing
2021-01-13 15:40:27 +08:00
crazywhalecc
ccadec23e4 update docs and change console command suitable 2021-01-07 16:01:01 +08:00
jerry
0972a1959e update README.md 2021-01-05 23:35:49 +08:00
crazywhalecc
ce74191947 update docs 2021-01-05 16:19:35 +08:00
crazywhalecc
4feeb9519c update docs 2021-01-04 16:59:19 +08:00
crazywhalecc
efee146215 update docs 2021-01-04 16:45:06 +08:00
jerry
96ce7b30d0 add InterruptException catcher to onRequest 2021-01-04 01:35:54 +08:00
jerry
076339baec move warning to framework 2021-01-03 18:16:35 +08:00
jerry
50026be73d fix a bug 2021-01-03 18:13:44 +08:00
jerry
a1ad634926 fix a bug 2021-01-02 20:00:33 +08:00
jerry
0a5defaf29 fix a bug 2021-01-02 19:44:19 +08:00
jerry
557efc47a8 update to 2.1.4 version 2021-01-02 19:40:13 +08:00
jerry
c566f940e0 add custom app version 2021-01-02 18:10:20 +08:00
jerry
381062c6c5 update to 2.1.3 version
fix an AnnotationParser bug which is stupid
2021-01-02 16:36:25 +08:00
jerry
b31876025e update composer.json 2021-01-02 13:35:45 +08:00
jerry
ae8b0acdaa update to 2.1.2 version 2021-01-02 13:35:24 +08:00
jerry
20ca3e7416 update docs 2021-01-02 13:28:26 +08:00
jerry
a1bfc031b8 update to 2.1.1 version 2021-01-02 13:20:16 +08:00
jerry
8a6f8f54a5 update composer.json path 2021-01-02 13:19:39 +08:00
jerry
19f0bffcd8 update composer.json 2021-01-02 13:16:22 +08:00
jerry
6337b626d6 update to 2.1.0 version
add @OnCloseEvent, @OnOpenEvent, @OnMessageEvent, @OnRequestEvent
update EventDispatcher.php and change it to status-based schema
fix @CQBefore bugs
2021-01-02 13:15:50 +08:00
crazywhalecc
7434bac94e update to 2.0.3 version 2020-12-31 16:26:51 +08:00
crazywhalecc
ac45ab0dec update to 2.0.2 version 2020-12-31 13:58:59 +08:00
crazywhalecc
8d248f301e update to 2.0.2 version 2020-12-31 13:56:59 +08:00
jerry
937d31ccd9 update docs 2020-12-31 01:44:09 +08:00
crazywhalecc
ef263a5d1e update docs 2020-12-31 00:35:40 +08:00
jerry
bf03dfda38 Merge remote-tracking branch 'origin/master' 2020-12-31 00:28:36 +08:00
jerry
6ba209c4c7 update docs 2020-12-31 00:28:16 +08:00
Whale
e7d86537be Update README.md 2020-12-26 21:04:48 +08:00
crazywhalecc
44979c670f update docs 2020-12-25 16:53:44 +08:00
Whale
747ecf28ea Merge pull request #23 from 854854321/master
[MOD] add global function -> getAllFdByConnectType  根据连接类型获取所有fd.
2020-12-25 16:41:48 +08:00
Whale
f5d5929cb9 Update global_functions.php 2020-12-25 16:41:14 +08:00
Whale
6e866001d6 Update global_functions.php 2020-12-25 16:40:02 +08:00
wenhao
775c275288 [MOD] 解决php Notice <PHP Notice: Undefined index: x-self-id> 2020-12-25 16:28:21 +08:00
wenhao
86a0e1a2ca [MOD] add global function -> getAllFdByConnectType 根据连接类型获取所有fd. 2020-12-25 15:48:41 +08:00
Whale
aae79aacb5 Update README.md 2020-12-25 01:42:23 +08:00
Whale
991af3d728 Update README.md 2020-12-25 01:41:23 +08:00
Whale
25ae4e02c2 Update README.md 2020-12-25 01:40:45 +08:00
Whale
5a6642e217 Update README.md 2020-12-25 01:40:24 +08:00
Whale
d00f6ee3a2 Update README.md 2020-12-25 01:27:19 +08:00
crazywhalecc
1e6cc9cd84 update docs 2020-12-23 16:20:35 +08:00
crazywhalecc
b7b94db5ea Merge remote-tracking branch 'origin/master' into master 2020-12-23 16:15:18 +08:00
crazywhalecc
b0054d7884 update docs 2020-12-23 16:14:59 +08:00
Whale
22942c33cf Update README.md 2020-12-23 11:27:23 +08:00
crazywhalecc
1f9c5eeeb4 add CNAME 2020-12-23 11:23:57 +08:00
crazywhalecc
44337d2ad4 update to 2.0.1 version
fix swoole log file "no such file or directory" error
2020-12-23 11:16:24 +08:00
91 changed files with 5813 additions and 539 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ composer.lock
/bin/.phpunit.result.cache
/resources/zhamao.service
.phpunit.result.cache
.daemon_pid

View File

@@ -1,7 +1,7 @@
<div align="center">
<img src="/resources/images/logo_trans.png" height = "150" alt="炸毛框架"><br>
<h2>炸毛框架</h2>
炸毛框架 (zhamao-frameowork) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
炸毛框架 (zhamao-framework) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)]()
[![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
@@ -20,6 +20,8 @@
**2.0 版本如果有问题请第一时间加群反馈!**
有关 3.0 版本的最新情况,请看这里:[Issue #22](https://github.com/zhamao-robot/zhamao-framework/issues/22)
## 简介
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
@@ -46,7 +48,7 @@ public function index() {
## 文档v2 版本)
查看文档:[https://docs-v2.zhamao.xin/](https://docs-v2.zhamao.xin/)
备用链接:[http://docs-v2.zhamao.me/](http://docs-v2.zhamao.me/)
备用链接:[https://docs-v2.zhamao.me/](https://docs-v2.zhamao.me/)
自行构建文档:`mkdocs build -d distribute`
@@ -58,7 +60,7 @@ public function index() {
- 易用的上下文,模块内随处可用
- 采用模块化编写,可单独拆装功能
- 常驻内存,全局缓存变量随处使用
- 自带 MySQL、Refis 等数据库连接池等数据库连接方案
- 自带 MySQL、Redis 等数据库连接池等数据库连接方案
- 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口
- 静态文件服务器
@@ -82,6 +84,8 @@ public function index() {
## 关于
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
作者的炸毛机器人已从2018年初起稳定运行了**三年**,并且持续迭代。
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ627577391或提交 Issue 进行疑难解答。
本项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的模块代码做好备份。
@@ -90,4 +94,4 @@ public function index() {
**注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)

View File

@@ -1,28 +1,14 @@
#!/usr/bin/env php
<?php
use ZM\ConsoleApplication;
// 这行是用于开发者自己电脑的调试功能
$symbol = sha1(is_file("/flag2") ? file_get_contents("/flag2") : '1') == '6252c0ec7fcbd544c3d6f5f0a162f60407d7a896' || mb_strpos(getcwd(), "/private/tmp");
// 首先得判断是直接从library模式启动的框架还是从composer引入library启动的框架
// 判断方法:判断当前目录上面有没有 /vendor 目录,如果没有 /vendor 目录说明是从 composer 引入的
// 否则就是直接从 framework 项目启动的
if (!is_dir(__DIR__ . '/../vendor') || $symbol) {
if (!is_dir(__DIR__ . '/../vendor')) {
define("LOAD_MODE", 1); //composer项目模式
define("LOAD_MODE_COMPOSER_PATH", getcwd());
/** @noinspection PhpIncludeInspection */
require_once LOAD_MODE_COMPOSER_PATH . "/vendor/autoload.php";
} elseif (substr(__DIR__, 0, 7) == 'phar://') {
define("LOAD_MODE", 2); //phar模式
// 会废弃phar启动的方式在2.0
} else {
define("LOAD_MODE", 0);
define("LOAD_MODE", 0); //源码模式
require_once __DIR__ . "/../vendor/autoload.php";
}
// 终端的命令行功能启动!!
$application = new ConsoleApplication("zhamao-framework");
$application->initEnv();
$application->run();
(new ZM\ConsoleApplication("zhamao-framework"))->initEnv()->run();

View File

@@ -3,8 +3,12 @@
"description": "High performance QQ robot and web server development framework",
"minimum-stability": "stable",
"license": "Apache-2.0",
"version": "2.0.0",
"extra": {},
"version": "2.2.0",
"extra": {
"exclude_annotate": [
"src/ZM"
]
},
"authors": [
{
"name": "whale",
@@ -33,7 +37,8 @@
"zhamao/config": "^1.0",
"zhamao/request": "*@dev",
"symfony/routing": "^5.1",
"symfony/polyfill-php80": "^1.20"
"symfony/polyfill-php80": "^1.20",
"ext-posix": "*"
},
"suggest": {
"ext-ctype": "*",
@@ -47,16 +52,7 @@
"src/ZM/global_functions.php"
]
},
"autoload-dev": {
"psr-4": {
"ZMTest\\": "test/ZMTest"
},
"files": [
"test/ZMTest/Mock/mock.php"
]
},
"require-dev": {
"phpunit/phpunit": "^9.3",
"swoole/ide-helper": "@dev"
}
}
}

View File

@@ -36,13 +36,19 @@ $config['swoole'] = [
/** 轻量字符串缓存,默认开启 */
$config['light_cache'] = [
'size' => 1024, //最多允许储存的条数需要2的倍数
'max_strlen' => 16384, //单行字符串最大长度需要2的倍数
'size' => 512, //最多允许储存的条数需要2的倍数
'max_strlen' => 32768, //单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'].'_cache.json',
'auto_save_interval' => 900
];
/** 大容量跨进程变量存储2.2.0可用) */
$config["worker_cache"] = [
"worker" => 0,
"transaction_timeout" => 30000
];
/** MySQL数据库连接信息host留空则启动时不创建sql连接池 */
$config['sql_config'] = [
'sql_host' => '',
@@ -72,7 +78,7 @@ $config["access_token"] = '';
/** HTTP服务器固定请求头的返回 */
$config['http_header'] = [
'X-Powered-By' => 'zhamao-framework',
'Server' => 'zhamao-framework',
'Content-Type' => 'text/html; charset=utf-8'
];

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
docs-v2.zhamao.me

View File

@@ -0,0 +1,143 @@
# 接入 WebSocket 客户端
炸毛框架其实从本质上讲,就是一个 HTTP + WebSocket 服务器,所以框架也支持对接其他任何 HTTP 客户端和 WebSocket 客户端,实际上炸毛框架非常适合用 WebSocket 做在线的 IM 聊天通讯,也可以方便地进行 WS 通信。这里主要说明如何对接一个自定义的 WebSocket 客户端。
## 类型指定
由于 WebSocket 连接都具有同样的性质,没有状态,所以在建立 WebSocket 连接的时候,需要客户端表明自己的身份和类型。指定客户端连接类型的方式有两种:
- `GET` 参数传递,在连接的时候,加上 GET 参数 `type` 即可。比如 js 中 WebSocket 建立时地址写:`ws://127.0.0.1:20001/?type=foo`,这时传入的连接就是 `foo` 类型。
- `Header` 传递,用户需要在建立连接时指定 HTTP 的头部信息 `X-Client-Role`,例如 `X-Client-Role: foo`,这时传入的连接就是 `foo` 类型。
以上两种方式,`Header` 方式比 `GET` 方式优先级要高,如果两者均没有指定,框架会将此连接当作 `default` 类型接入。
!!! note "提示"
对于对接 OneBot 标准的机器人客户端,只要符合 OneBot 标准,即 `X-Client-Role` 会自动带上 `universal``qq` 等字样,就会自动标记为 `qq` 类型。
## 逻辑编写
传入连接后,我们就能通过注解事件绑定来做我们自己想做的事情了!比如下方是传入类型为 foo 连接要做的事情
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Console\Console;
use ZM\ConnectionManager\ConnectionObject;
class Hello {
/**
* @OnOpenEvent("foo")
*/
public function onFooConnect(ConnectionObject $conn) {
Console::info($conn->getName()." 已连接!");
}
```
以上作用就是在终端输出 `foo 已连接!` 这个提示的。关于 `ConnectionObject` 对象,见下方。
## WS 连接对象
对于每一个 WebSocket 连接,框架内都有一个专属的操作类,有获取类型名称、保存链接参数和属性以及获取文件标识符等功能。
### getFd()
获取文件标示符,用于发送消息、接收消息等。这个参数获取的 `fd` 是 Swoole 指定的,用于发送信息等。
```php
$fd = $conn->getFd();
server()->send($fd, "hello world");
```
> WebSocket 是全双工的,所以发送和接收其实是互不干扰的,你可以不仅仅在 WebSocket 相关的上下文中,还可以比如在 HTTP 或者机器人上下文中给别的 WebSocket 客户端发请求。
### getName()
获取连接对象绑定的连接类型,例如上方提到的 `foo``default` 等。
```php
Console::info("当前连接类型:".$conn->getName()); //当前连接类型foo
```
### setName()
改变连接对象绑定的连接类型,例如从 `foo` 改为 `bar`
```php
$s = $conn->getName(); // foo
$conn->setName("bar");
$s = $conn->getName(); // bar
```
### getOptions()
获取此连接存储的所有参数,以数组形式。存储内容见下方 `setOption()`
格式:`["参数1" => {参数1的值}, "参数2" => {参数2的值}]`
### getOption()
获取此连接存储的参数,获取指定名称的,此方法拥有一个参数 `$key`,指定即可获取。
如果没有对应参数,则返回 `null`
我们在前面的机器人部分知道,框架主要是用于机器人的连接,那么机器人客户端在连接后,比如我们想知道这个机器人的 WS 连接对应的是哪个 QQ 号的机器人,我们就可以用 `getOption("connect_id")` 来获取。这个 `connect_id` 是 OneBot 标准的客户端接入后自动填入的一个参数。例如,我们想在机器人接入后打出接入机器人的 QQ 号:
```php
/**
* @OnOpenEvent("qq")
*/
public function onQQConnect($conn) {
Console::success("机器人 ".$conn->getOption("connect_id")." 已连接!"); // 机器人 123456 已连接!
}
```
### setOption()
设置连接存储的参数。参数:`setOption($key, $value)``$key` 限定为 `connect_id` 一种。(因为目前有了 LightCache所以这里暂时不提供别的 key 设定)
```php
$conn->setOption("connect_id", "asdasdasd"); // $value 最长长度为 29
```
## 发送到 WebSocket 客户端
很简单,从上面获取到 `fd` 后使用下面的方式就可以了~
```php
server()->push($conn->getFd(), "hello"); // 第二个为 string 类型的参数
```
## 从客户端接收
接收消息必须从 `@OnMessageEvent` 注解事件下接收,使用上下文 `ctx()->getFrame()` 获取消息帧。
从这里获取的 `Frame` 对象,见 [Swoole 文档 - Frame](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)。
Frame 对象有四个参数:
- `$frame->fd`:获取发来帧的 fd
- `$frame->data`:数据本体
- `$frame->opcode`:数据类型 int 值,见 [Swoole 文档 - 数据帧类型](https://wiki.swoole.com/#/websocket_server?id=%e6%95%b0%e6%8d%ae%e5%b8%a7%e7%b1%bb%e5%9e%8b)
- `$frame->finish`是否发送完毕bool
下面以接收一个 json 字符串为例,并进行后续的解析:
```php
/**
* @OnMessageEvent("foo")
*/
public function onMessage() {
$frame = ctx()->getFrame();
$json_str = $frame->data; // 假设传入的是 {"key1":"value1","k2":"v2"}
$json = json_decode($json_str, true);
Console::info("key1 的值是:" . $json["key1"]);
}
```
## 关闭连接
```php
server()->close($conn->getFd());
```

View File

@@ -0,0 +1,119 @@
# 框架高级启动
## 框架下载方式
从前面的几章中,我们了解到框架有多种下载到本地的方式。
- Composer 依赖模式
- Starter 从模板创建模式
- 源码模式
### Composer 依赖模式
从 Composer 依赖加载框架是一种拉取框架的方式,这种方式的优点在于,你可以直观地感受到是如何使用框架从零开始一个完整的项目的过程。
从 Composer 依赖的启动步骤:
```bash
mkdir my-bot # 新建一个空的文件夹
cd my-bot/
composer require zhamao/framework # 从 composer 拉取后会自动部署 autoload 和 composer.json 等内容
# 使用命令初始化框架
vendor/bin/start init
# 启动框架
vendor/bin/start server
```
注意:使用 `init` 命令时,会给当前目录解压以下文件:
```php
$extract_files = [
"/config/global.php", // 全局配置文件
"/.gitignore", // git 排除文件
"/config/file_header.json", // HTTP 文件头
"/config/console_color.json", // 终端颜色主题文件
"/config/motd.txt", // 框架启动时自定义的 motd
"/src/Module/Example/Hello.php", // 框架自带的示例模块
"/src/Module/Middleware/TimerMiddleware.php", // 框架自带的函数运行时间监控中间件
"/src/Custom/global_function.php" // 用户可在这里自定义编写自己的全局函数
];
```
经过 init 解压这些文件后,你的框架就能正常运行且开始编写代码了!
### Starter 模板模式
从模板新建其实原理和 Composer 依赖模式完全一样,只不过,这个过程是使用模板仓库新建的项目,使用 Composer 自带的 `create-project` 方式创建的。starter 也是一个 GitHub 项目,见 [地址](https://github.com/zhamao-robot/zhamao-framework-starter)。
```bash
composer create-project zhamao/framework-starter my-bot/ # my-bot 是你自定义的文件夹名称,和上方相同
cd my-bot
vendor/bin/start server # 启动框架
```
Starter 模式相当于直接从 GitHub 拉取 `zhamao-framework-starter` 项目,然后执行 `composer update`
那和 Composer 依赖模式有什么区别呢?没区别!构建出来的框架和文件是一模一样的!使用 Composer 依赖模式,使用 `init` 命令后,文件会和 `zhamao-framework-starter` 仓库拉取回来的模板一模一样!(或者换句话说,这个仓库就是使用 `init` 命令生成的文件的)
那使用哪种好呢?看你自己!如果你想给你自己的已有项目套上炸毛框架,那么就推荐使用 Composer 依赖模式,如果是从 0 开始编写框架模块,则推荐使用模板模式。
### 源码模式
源码模式和以上两种方案都不一样,源码模式允许你对框架本身进行一系列修改,框架本体就可以直接运行。
Composer 依赖模式(以及模板模式)和源码模式的区别是:
- 依赖模式和模板模式是通过 library 方式引入框架的,框架本身会放在 composer 的 `vendor/` 目录下,从 composer 引入的 library 相当于子集vendor 目录下的文件最好不要手动修改(应该都知道吧),所以框架本身也只是加载了进来。
- 源码模式相当于直接从框架源码目录运行框架和模块,框架源码都在 `src/ZM` 目录下,默认的示例模块都在 `src/Module` 下,是同级目录。而此时的 `vendor/` 目录只包含了框架依赖的外部组件,例如注解解析器和 psysh 等。
源码模式可以方便地调试和修改框架本身,拉取方式很简单,用 `git clone` 或从 GitHub 下载最新版的源码包解压即可。
```bash
git clone https://github.com/zhamao-robot/zhamao-framework.git
cd zhamao-framework/
bin/start server # 第一次运行时会提示一个“框架源码模式需要在autoload文件中添加Module目录为自动加载”
composer update # 更新 autoload 文件,应用刚才上一步添加的 `src/Module` 文件夹下的模块自动加载
bin/start server # 通过源码模式启动框架
```
## 框架启动参数
框架启动时可以根据实际情况指定启动参数。
- `--debug-mode`:启用调试模式,调试模式的作用是关闭一键协程化和终端交互,减少 Swoole 本身对代码逻辑的干扰(比如执行 `shell_exec()` 报错的话可以开启这个进行调试)。
- `--log-{mode}`:设置 log 等级。支持 `--log-debug``--log-verbose``--log-info``--log-warning``--log-error`
- `--log-theme`:设置终端信息的主题。这个选项适用于多种终端信息显示的兼容,例如白色终端和不支持颜色的终端。详见 [Console - 主题设置](/component/console/#_2)。
- `--disable-console-input`:关闭终端交互,如果你使用的不是 tmux、screen 而是直接将进程使用 systemd 等方式运行到 init 守护进程下,则需要关闭终端交互输入,关闭后不可以使用 `stop, reload, logtest` 等交互命令。
- `--disable-coroutine`:关闭一键协程化。
- `--daemon`:以守护进程方式运行框架,此参数将直接在输出 motd 后将进程挂到 init 下运行,后台常驻。
- `--watch`:监控 `src/` 目录下的文件变化,有变化则自动重新载入代码。开启监控需要安装 PHP 扩展inotify。使用 pecl 就可以安装:`pecl install inotify`
- `--env`:设置运行环境,设置运行环境后将优先加载指定环境的配置文件,支持 `--env=production``--env=staging``--env=development`,见 [基本配置](/guide/basic-config/#_2)。
## 守护进程操作命令
守护进程在 2.2.0 版本开始,可以使用命令行快速操作,如重启、停止、查看状态等。
注意,这里的守护进程操作命令是指 **使用 `--daemon` 方式启动的框架**,如使用 Docker、screen、tmux 等方式挂后台跑则此命令不可用!
```bash
vendor/bin/start daemon:status # 查看守护进程的状态
vendor/bin/start daemon:reload # 重载框架
vendor/bin/start daemon:stop # 停止运行守护进程的框架
```
## 独立启动其他组件
框架默认不止启动框架的 `server` 命令,还有 `init` 命令和 `simple-http-server` 命令。`init` 命令在上方 Composer 依赖模式中提到过,就是初始化各个文件的。
### 独立 HTTP 文件服务器
如果你只需要一个静态文件服务器,类似 Nginx那么框架也支持。
```bash
vendor/bin/start simple-http-server your-web-dir/ --host=0.0.0.0 --port=8080
```
- `your-web-dir` 是必填的参数。
- `--host``--port` 是可选参数,如果不填,则默认使用 `global.php` 配置文件中的配置。

View File

@@ -0,0 +1,6 @@
# 框架剖析
## 框架运行总结构图
![](../assets/img/framework-structure.png)

View File

@@ -1,3 +1,9 @@
# 进阶开发
## 深入
还没填坑,敬请期待!
在本章,下面的部分将详细说明一些具体的案例和自定义框架的操作。
- 如何自定义修改框架本身?- [框架启动方式](/advanced/custom-start/)
- 如何接入一个自己的 WebSocket 客户端?- [接入 WebSocket 客户端](/advanced/connect-ws-client/)
- 框架到底是怎么工作的?- [框架结构剖析](/advanced/framework-structure/)
> 更多进阶教程敬请期待....(或者你可以选择提 Issue 到框架 GitHub有需求就写入文档

View File

@@ -0,0 +1,29 @@
# 内部类文件手册
这个章节写明了在框架使用过程中可能涉及到的框架内部或 Swoole、其他 composer 依赖组件的内部类,这里会根据类的命名空间一一说明。
## Swoole\Http\Request
此类是 Swoole 内部的一个类,一般在收到 HTTP 请求时,在 `@RequestMapping``@OnRequestEvent()` 两个注解下可用,用作获取 GET、POST参数上传到后端的文件、Cookies 等。详见 [Swoole 文档 - Request](http://wiki.swoole.com/#/http_server?id=httprequest) 。
### 属性
- `$fd`:获取当前连接的文件描述符 ID。
- `$header``HTTP` 请求的头部信息。类型为数组,所有 `key` 均为小写。
- `$server``HTTP` 请求相关的服务器信息。
- `$cookie`:获取 Cookies。
- `$get`:获取 GET 参数。
- `$post`:获取 POST 参数。
- `$files`:获取上传的文件信息
### 方法
- `rawContent()`:获取 POST 包原始二进制内容,相当于原生 PHP 的 ` file_get_contents("php://input");`
- `getData()`:获取完整的原始 `Http` 请求报文。包括 `Http Header``Http Body`
### 示例
```php
TODO先放一放。
```

View File

@@ -29,6 +29,17 @@
justify-content: flex-end;
}
.doc-chat-banner {
justify-content: center;
background: rgba(0,0,0,0.1);
width: max-content;
margin: 8px auto;
padding: 4px 14px;
border-radius: 8px;
color: gray;
font-size: 14px;
}
.doc-chat-row-robot {
justify-content: flex-start !important;
}
@@ -85,4 +96,8 @@
width: 36px !important;
height: 36px !important;
border-radius: 18px;
}
.md-typeset .admonition, .md-typeset details {
font-size: .72rem;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

65
docs/component/atomics.md Normal file
View File

@@ -0,0 +1,65 @@
# ZMAtomic 原子计数器
原子计数器是用于多进程间跨进程使用的原子计数使用的,比如统计入站请求数量等。此功能基于 Swoole 的 Atomic详情见 [Swoole - 文档]([进程间无锁计数器(Atomic) (swoole.com)](http://wiki.swoole.com/#/memory/atomic))。
## 配置和初始化
见配置文件:`config/global.php` 中的 `init_atomics` 字段:
```php
/** zhamao-framework在框架启动时初始化的atomic们 */
$config['init_atomics'] = [
'foo' => 0,
'bar' => 4,
];
```
这时我们就成功初始化两个原子计数器,名字分别为 `foo``bar`
!!! warning "注意"
初始化的值必须是不小于 0 的 int32 值!
## 使用
定义和命名空间:`ZM\Store\ZMAtomic`
连接计数示例:
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Store\ZMAtomic;
class Hello {
/**
* @OnRequestEvent()
*/
public function onRequest() {
$cnt = ZMAtomic::get("foo")->add(1);
ctx()->getResponse()->end("当前已访问:".$cnt."");
}
}
```
### ZMAtomic::get()->get()
获取计数的数字:`dump(ZMAtomic::get("bar")->get());` 返回 4。
### ZMAtomic::get()->add($num)
加上一定的数并返回结果:`dump(ZMAtomic::get("bar")->add(5));` 返回 9。
### ZMAtomic::get()->sub($num)
要减少的数值(必须为正整数):`dump(ZMAtomic::get("bar")->sub(5));` 返回 5。
### ZMAtomic::get()->set($num)
设置计数的数字:`ZMAtomic::get("bar")->set(77);`
!!! note "提示"
还有一些不常用的方法,可以看 Swoole 官方的文档,这里就不一一列举了。

177
docs/component/console.md Normal file
View File

@@ -0,0 +1,177 @@
# Console 控制台
Console 类所在命名空间:`\ZM\Console\Console`
Console 类为框架的终端输出管理类。
## 设置 Log 输出等级
**输出等级** 控制了输出到命令行的内容的重要性。在框架的输出中,消息有以下几种不同等级的类别
- **error** / **log**: 0
- **warning**: 1
- **info** / **success**: 2
- **verbose**: 3
- **debug**: 4
输出等级设置后显示的消息类别为小于等于当前 log 的。假设你将 log 等级设置为 3你可以看到除 debug 外的所有 log 内容。
通过配置文件 `global.php` 中的 `init_atomics -> info_level` 的数值你可以更改框架的默认 log 等级(默认为 2
你也可以在启动框架的命令行中添加参数来切换 log 等级:
```bash
vendor/bin/start server --log-error # 以 error 等级启动框架
vendor/bin/start server --log-warning # 以 warning 等级启动框架
vendor/bin/start server --log-info # 以 info 等级启动框架
vendor/bin/start server --log-verbose # 以 verbose 等级启动框架
vendor/bin/start server --log-debug # 以 debug 等级 启动框架
```
## 使用 Log 输出内容
作为模块开发者的你,你可以主动调用框架内的 Console 类输出信息到终端。
### Console::log()
输出 0 级别的普通 log。
- 参数:`$msg, $color`,分别为内容和字体颜色。
> 此 log 不会被 info_level 所限制,无论如何也会输出到终端。
### Console::error()
输出 error 级别的红色醒目 log。一般此 log 为框架内部出现不可忍受的错误比如内存不足、PHP fatal error 等错误。
- 参数:`$msg`
> 此 log 不会被 info_level 所限制,无论如何也会输出到终端。
### Console::warning()
输出 warning 级别的 log。
!!! warning 注意
框架内出现的用户态异常,比如无法发送 API、无法连接数据库等错误都是 warning 错误,不会导致框架崩溃或功能错误的异常情况建议都使用 warning 输出而不是 error。
### Console::info()
输出 info 级别的 log。
### Console::success()
输出 success 级别的log。
### Console::verbose()
输出 verbose 级别的 log。
### Console::debug()
输出 debug 级别的 log。
### Console::stackTrace()
输出栈追踪信息。
### Console::setColor()
返回:彩色的字符串。
- **string**: 要变颜色的字符串
- **color**: 要变的颜色。支持 `red``green``yellow``reset``blue``gray``gold``pink``lightblue``lightlightblue`
```php
Console::log("This is normal msg. (0)");
Console::error("This is error msg. (0)");
Console::warning("This is warning msg. (1)");
Console::info("This is info msg. (2)");
Console::success("This is success msg. (2)");
Console::verbose("This is verbose msg. (3)");
Console::debug("This is debug msg. (4)");
Console::stackTrace();
$str = Console::setColor("I am gold color.", "gold");
```
## 终端交互命令
炸毛框架支持从终端输入命令来进行一些操作,例如重启框架、停止框架、执行函数等。
::: warning 注意
在 Docker、systemd、daemon 状态下启动的框架会自动关闭终端等待输入,交互不可用。
:::
### reload
重新加载除 `src/Framework/` 下的所有模块。
- 别名:`r`
### stop
停止框架。
### logtest
输出各种等级的 log 示例文本。
### call
执行对应类的成员方法。下面是例子:
```bash
call \ZM\Utils\ZMUtil reload
```
### bc
直接执行 PHP 代码,输入格式为 base64。
```bash
bc XEZyYW1ld29ya1xDb25zb2xlOjp3YXJuaW5nKCJoZWxsbyB3YXJuaW5nISIpOw==
# 代码内容:\ZM\Console\Console::warning("hello warning!");
# 终端输出:[19:14:32] [W] hello warning!
```
### echo
输出文本
```bash
echo hello
```
### color
按照颜色输出文本
```bash
color green 我是绿色的字
```
## MOTD
在 1.4 版本开始,框架支持启动时的 motd 内容修改。
文件位置:`config/motd.txt`
## 设置输出主题
Console 组件支持为多种不同的终端设置不同的主题,比如有些人喜欢使用白色的终端,但是白色终端下 info 的颜色很浅,看不到,还有人使用不能显示颜色的黑白终端.....
```bash
vendor/bin/start server --log-theme={主题名}
```
现有支持的主题有:`default``white-term``no-color`
```bash
vendor/bin/start server --log-theme=white-term # 如果用的是白色终端,这个主题更友好
vendor/bin/start server --log-theme=no-color # 如果不想让 log 带有任何颜色,使用无色主题
```

424
docs/component/context.md Normal file
View File

@@ -0,0 +1,424 @@
# 上下文
上下文作为整个框架中最重要的内容之一,请务必理解和完整地阅读此部分!
一个上下文描述了一个事件和所关联的对象的环境。例如:你在处理 HTTP 请求的 `@RequestMapping` 绑定的事件中,你需要获取请求的 HTTP 头和 Cookie再比如你在处理 QQ 机器人发来的命令 `@CQCommand("随机数")` 的时候,在这个方法内,你需要获取发来的人的 QQ 号码。以上我们将处理以上运行环境的对象叫做上下文。
由于 Swoole 的协程加持,我们利用了协程 ID 绑定对象来进行构造上下文。
以默认的机器人收发消息为例,通过对默认模块的了解,我们可以知道,在绑定 `@CQCommand` 等类似事件后,你可以用上下文获取发来这条消息的人的 QQ 号码:
```php
/**
* @CQCommand("你好")
*/
public function hello() {
$user_id = ctx()->getUserId();
ctx()->reply("你好啊,".$user_id.",很高兴认识你!");
}
```
`context()` 就是获取上下文对象的全局函数,它还有简写:`ctx()`
当然,上下文中的方法不是每个都能在任何时候使用的。例如 `getUserId()` 你不能在 `@RequestMapping` 注解的函数中使用,因为它不是机器人消息的上下文。下面说明上下文对象的方法中,每个都会说明每个方法可以在哪些事件中使用:
## getServer() - 获取 Server 对象
获取 Swoole WebSocker Server 对象。此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server)。
可以使用的事件:`@OnMessageEvent()``@OnOpenEvent()``@OnCloseEvent()``@OnStart()` 以及所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
## getFrame() - 获取 WS 数据帧
获取 `\Swoole\Websocket\Frame` 对象,此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)。
可以使用的事件:`@OnMessageEvent()` 以及所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等,
## getFd() - 返回 fd 值
获取当前连入 Swoole 服务器的连接文件描述符 ID。返回 int。一般代表连接号可用来绑定对应链接。
可以使用的事件:所有 **getFrame()** 可以使用的,`@OnOpenEvent()``@OnCloseEvent()`
!!! tip "提示"
值得注意的是,由于机器人客户端和炸毛框架的连接是通过 WebSocket 进行的,而 WebSocket 是长连接,所以同一个机器人一次连接下收发消息所用的连接是同一个,所以 Fd 也是相同的。同理,炸毛框架的内部来区分多个机器人也是通过这一 Fd 进行判定的。
=== "代码"
```php
/**
* @CQCommand("测试fd")
*/
public function testfd() {
ctx()->reply("当前机器人连接的fd是".ctx()->getFd()"机器人QQ是".ctx()->getRobotId());
}
```
=== "效果"
<chat-box>
^ 假设我们和连接55555的机器人的私聊
) 测试fd
( 当前机器人连接的fd是1机器人QQ是55555
^ 假设切到了另一个机器人66666的私聊
) 测试fd
( 当前机器人连接的fd是2机器人QQ是66666
</chat-box>
## getData() - 获取事件完整数据
返回 CQHTTP 事件上报的原始数据包,已经被解析成数组,可以直接操作。
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
```php
/**
* @CQMessage(user_id=123456)
*/
public function onMessage() {
$data = ctx()->getData();
ctx()->reply("消息类型是:" . $data["message_type"]);
}
```
<chat-box>
^ 假设我是QQ为123456的用户私聊发消息
) 哈咯!!
( 消息类型是private
</chat-box>
## getRequest() - HTTP 请求对象
返回 `\Swoole\Http\Request` 对象,可在 `@RequestMapping` 中使用,获取 Cookie请求头GET 参数什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httprequest)。
可以使用的事件:`@RequestMapping()``@OnRequestEvent()``@OnOpenEvent()`。
## getResponse() - HTTP 响应对象
返回 `\Swoole\Http\Response` 对象的增强版,可在 HTTP 请求相关的事件中使用,返回内容和设置 Cookie 什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httpresponse)。
可以使用的事件:`@RequestMapping()``@OnRequestEvent()`。
下面是使用以上两个功能的组合示例:
```php
/**
* @RequestMapping("/ping")
*/
public function ping() {
$name = ctx()->getRequest()->get["name"] ?? "unknown";
ctx()->getResponse()->end("Hello ".$name."!");
}
```
## getConnection() - WS 连接对象
返回此上下文相关联的 WebSocket 连接对象。详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client)。
可以使用的事件:所有 **getFrame()** 可以使用的都可以使用。
## getCid() - 上下文 ID
返回当前上下文所绑定的协程 ID此 ID 和 `\Co::getCid()` 返回值一样。
## getRobot() - 获取机器人 API 对象
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](robot-api.md)。
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
```php
ctx()->getRobot()->sendPrivateMsg(123456, "发送私聊消息");
```
<chat-box>
^ 正在和机器人聊天
( 发送私聊消息
</chat-box>
## getMessage() - 获取消息
获取 data 数据中的 `message` 消息,用于快速获取用户消息事件的消息内容。
可以使用的事件:`@CQCommand()``@CQMessage``@CQBefore("message")``@CQAfter("message")`
=== "代码"
```php
/**
* @CQMessage(group_id=33333)
*/
public function groupRepeat() {
ctx()->reply(ctx()->getMessage());
}
```
=== "效果"
<chat-box>
^ 现在在群33333内机器人已经成了复读机
) 来世还做复读机!!!
( 来世还做复读机!!!
) 你不许复读!
( 你不许复读!
</chat-box>
## getUserId() - 获取用户 QQ 号
获取发消息的用户的 QQ 号码。
可以使用的事件:所有 **含有** `user_id` 上报参数的 OneBot 事件。
```php
/**
* @CQCommand("whoami")
*/
public function whoami() {
ctx()->reply("你是".ctx()->getUserId()); //返回你是123456
}
```
## getGroupId() - 获取 QQ 群号
获取发消息来自的 QQ 群号。
可以使用的事件:所有含有 `group_id` 上报参数的 OneBot 事件。
## getMessageType() - 消息类型
获取消息类型,同参数 `message_type`。
可以使用的事件:所有 `post_type` 为 `message` 的响应事件,如 `@CQMessage``@CQCommand`。
## getRobotId() - 机器人 QQ 号
获取事件上报的机器人自己的 QQ 号码。
可以使用的事件:所有 OneBot 发来的事件:`@CQCommand()``@CQNotice()` 等。
## setMessage() - 设置消息
与 `getMessage()` 对应,用于更改上下文中保存的事件信息,可以用于消息变更和过滤。
## setUserId() - 设置用户 ID
与上同理,更改 `user_id`。
## setGroupId() - 设置群号
与上同理。
## setMessageType() - 设置类型
与上同理,修改消息类型。
## setData() - 设置数据包
与上同理,与 `getData()` 对应,用于更改上下文中的 `data`。
## getCache() - 上下文缓存
获取保存在上下文中的临时缓存变量。当相关联的事件结束后,数据会从内存中被释放。用于同一事件的多个函数中的信息传递。
- 参数:`$key`,缓存变量的键名
- 返回:`mixed`,存入缓存的变量值。
```php
$a = ctx()->getCache("block_continue");
// 如果变量不存在,则返回 null
```
## setCache() - 上下文缓存
与 `getCache()` 对应,是设置内容的。
```php
ctx()->setCache("abc", "asdasd");
$result = ctx()->getCache("abc"); // asdasd
```
## reply() - 快速回复
快速回复当前用户消息内容。
- 参数1`$msg`,字符串,你要回复的消息内容
- 参数2`$yield = false`,可选,当为 `true` 时,会协程等待后返回 **消息回复** 的结果,包括 API 状态码、消息 `message_id` 等。
```php
$r = ctx()->reply("我又好了。");
if($r["retcode"] == 0) Console::success("消息发送成功!");
```
## finalReply() - 快速回复
快速回复用户消息,并阻止其他模块接下来继续处理此事件。
参数同 `reply()`。
## waitMessage() - 等待用户消息
- 参数:`waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "")`
- 用途:等待用户输入消息
`$prompt` 参数为回复用户的文本内容,`$timeout` 是等待用户回复的超时时间(秒)`$timeout_prompt` 是超时后回复用户的文本。
这个功能可以让开发机器人的代码逻辑和实际贴合,避免回调地狱、拼接参数、上下文脱节等问题,比如下方的示例,可以仅仅用两行代码实现一个问答式的对话过程。
用法示例:
```php
/**
* @CQCommand("自我介绍")
*/
function yourName(){
$r = ctx()->waitMessage("你叫啥名字呀?", 600, "你都10分钟不理我了嘤嘤嘤");
ctx()->finalReply("好的,可爱的机器人记住你叫 ".$r." 啦!以后多聊天哦!");
}
```
<chat-box>
) 自我介绍
( 你叫啥名字呀?
) jerry
( 好的,可爱的机器人记住你叫 jerry 啦!以后多聊天哦!
) 自我介绍
( 你叫啥名字呀?
^ 10分钟没理机器人
( 你都10分钟不理我了嘤嘤嘤
</chat-box>
## getArgs() - 自动获取参数
为 `waitMessage()` 的封装,目的是让机器人的回复更加智能化。最好的例子就是在框架自带的默认示例中“随机数”的例子,我们假设要写一个随机数功能,但是用户从来都是不思考就使用机器人的。抛开人工智能,我们能做的就是“专家系统”,同时让我们写的代码尽可能适配用户所说的每一句话:
- 随机数 1 100
- 随机数(一般不知道怎么用这个功能的人都会只说一个关键词)
- 从2到9的随机数
所以,在匹配第一和第二种情况时候,我们不需要重复写代码,而第一种的话用户已经将参数给你的时候,你不需要再次使用 `waitMessage()` 方式进行等待询问,只需要取到使用就好了。`getArgs()` 就是做这个的。
定义:`getArgs($mode, $prompt_msg)`
`$mode`:获取模式,有三种:
- `ZM_MATCH_ALL`:效果等同于 `getFullArg()`,获取全部的内容,把空格也当作一部分
- `ZM_MATCH_NUMBER`:效果等同于 `getNumArg()`,获取下一个数字参数
- `ZM_MATCH_FIRST`:效果等同于 `getNextArg()`,获取下一个参数
`$prompt_msg`:字符串,指定如果参数缺失时询问用户的内容。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getArgs(ZM_MATCH_FIRST, "请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<chat-box>
) test
( 请输入你要传入的参数内容
) test2
( 参数内容test2
</chat-box>
`getArgs()` 也有三层封装,在使用过程中避免麻烦的话,推荐使用下面这几种 `get*Arg()` 方式。
## getFullArg()
获取关键词后的整个字符串参数,包括空格,如果不存在则询问。
典型例子:`复读机 你好 你好`,获取参数时会将 `你好 你好` 当作一个参数来获取。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getFullArg("请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<chat-box>
) test abc def argtest
( 参数内容abc def argtest
) test
( 请输入你要传入的参数内容
) abc def
( 参数内容abc def
</chat-box>
## getNextArg()
获取下一个参数分隔符可以是空格tab。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getNextArg("请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<chat-box>
) test abc def argtest
( 参数内容abc
) test
( 请输入你要传入的参数内容
) abc
( 参数内容abc
</chat-box>
## getNumArg()
> 2.1.5 版本起可用。
获取下一个数字型参数,如果 `is_numeric()` 为 true 则获取成功,如果没有符合的则询问用户。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getNextArg("请输入你要传入的数字内容");
return "数字参数内容:".$s;
}
```
<chat-box>
) test abc 334 argtest
( 数字参数内容334
) test abc
( 请输入你要传入的数字内容
) 998
( 参数内容998
</chat-box>
## copy()
获取整个上下文的所有内容的数组形式。
```php
$arr = ctx()->copy();
dump($arr);
```
## getOption() - 获取匹配参数内容
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
return "参数内容:".implode(", ", ctx()->getOption());
}
```
<chat-box>
) test abc 334 argtest
( 参数内容abc, 334, argtest
</chat-box>

View File

@@ -0,0 +1,63 @@
# 协程池
首先要声明的一点是,协程池这个概念是我自己编的。
因为协程的特点,它是单线程下运行的,所以在一个进程内同时实际上只会有一个协程的代码在执行逻辑,但是后面的 IO 操作、协程挂起等待的操作都是同时去做的,比如数据库的大数据读取、写入需要耗时几秒甚至几十秒,这时用基于协程的 MySQL 连接池就完全不是问题。
但是就拿 MySQL 举例,我们 MySQL 使用的是 TCP 连接,而无论是 MySQL 还是 TCP 连接,最大数量都是有限的。我们即使设置了允许最大协程数量非常大,比如上百万,但是也不能让数据库连接池一个池支持上百万的连接。
这时假设高并发进来了怎么办呢?这时就需要框架提出的一个折中方案:协程池了。
顾名思义,协程池是一个容纳协程的区域,而协程里又容纳着各种各样需要阻塞调用被协程调用的 IO 操作,协程池用作限制协程的数量。
```php
use ZM\Utils\CoroutinePool;
use ZM\DB\DB;
// 传统写法,一旦高并发则可能导致 Too many connections
go(funuction(){
DB::rawQuery("INSERT INTO users VALUES(?,?)", ["admin", "password"]);
});
// 协程池写法
CoroutinePool::go(function(){
DB::rawQuery("INSERT INTO users VALUES(?,?)", ["admin", "password"]);
}, "foo");
```
参数:`go(callable $func, $name = "default")`
`$name` 为协程池对应的名字,你可以设置多个协程池,用来支持不同的需要限制并发 IO 数量的地方,例如 Redis 和 MySQL 设置不同的名字。`$func` 可为闭包或可调用的方法名称或数组。
## 配置
默认情况下,直接调用 `CoroutinePool::go()` 时,协程池大小为 30也就是如果有 30 个协程进入了挂起状态(比如数据库在执行查询语句),那么更多的协程执行时就会阻塞并以协程等待的方式等待,直到现有的 30 个协程中的一部分完成了它的工作。
## 方法
### CoroutinePool::go()
将协程放入协程池运行。
如果不写 `$name` 参数,则使用的是默认协程池。
### CoroutinePool::defaultSize()
设置默认协程池的大小(默认 30
```php
CoroutinePool::defaultSize(64);
for($i = 0; $i < 1000; ++$i) {
CoroutinePool::go(function(){
DB::rawQuery("SELECT * FROM users");
});
}
```
### CoroutinePool::setSize()
定义:`setSize($name, int $size)`
`$name` 为字符串,是你要用的协程池的名称。
`$size` 为大小,最大不可超过 Swoole 配置文件中指定的最大协程数量。

459
docs/component/cqcode.md Normal file
View File

@@ -0,0 +1,459 @@
# CQ 码(多媒体消息)
消息中的多媒体内容使用 CQ 码来表示,形如 `[CQ:face,id=178]`。其中,`[CQ:]` 是固定格式;`face` 是「功能名」,除了 `face` 还有许多不同的功能名;`id=178` 是「参数」,某些功能不需要参数,而另一些需要多个参数,当有多个参数时,参数间使用逗号分隔。
## 格式
一些 CQ 码的例子如下:
```
[CQ:shake]
[CQ:face,id=178]
[CQ:share,title=标题,url=http://baidu.com]
```
更多 CQ 码功能请参考 [消息段类型](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md)。
!!! warning "注意"
CQ 码中不应有多余的空格,例如不应该使用 `[CQ:face, id=178]`
CQ 码的参数值可以包含空格、换行、除 `[],&` 之外的特殊符号等。在解析时,应直接取 `[CQ:` 后、第一个 `,``]` 前的部分为功能名,第一个 `,` 之后到 `]` 之间的部分为参数,按 `,` 分割后,每个部分第一个 `=` 前的内容为参数名,之后的部分为参数值。例如 `[CQ:share,title=标题中有=等号,url=http://baidu.com]` 中,功能名为 `share``title` 参数值为 `标题中有=等号``url` 参数值为 `http://baidu.com`
## 转义
CQ 码中包含一些特殊字符:`[``]``,` 等,而 CQ 码又是可能混杂在纯文本内容之中的,因此消息中的纯文本内容需要对特殊字符进行转义,以避免歧义。具体的转义规则如下:
| 转义前 | 转义后 |
| ------ | ------- |
| `&` | `&amp;` |
| `[` | `&#91;` |
| `]` | `&#93;` |
另一方面CQ 码内部的参数值也可能出现特殊字符,也是需要转义的。由于 `,`(半角逗号)在 CQ 码中用于分隔参数,因此除了上面的转义规则,还需要对 `,` 进行转义,如下:
| 转义前 | 转义后 |
| ------ | ------- |
| `&` | `&amp;` |
| `[` | `&#91;` |
| `]` | `&#93;` |
| `,` | `&#44;` |
例如,一个链接分享消息的 CQ 码可能如下:
```
[CQ:share,title=震惊&#44;小伙睡觉前居然...,url=http://baidu.com/?a=1&amp;b=2]
```
## 封装调用
框架提供了 CQ 码的封装,你可以在任何位置使用封装好的 CQ 码生成器。
生成器是一个静态类,里面的方法全部是静态调用,命名空间是:`ZM\API\CQ`
例如,给用户发送图片这样写就好啦!只需要将添加图片的地方拼到回复用户的字符串里。如果只发图片,整个字符串里只能有 CQ 码。
```php
<?php
namespace Module\Example;
use ZM\API\CQ;
use ZM\Annotation\CQ\CQCommand;
class Hello {
/**
* @CQCommand("发送图片")
*/
public function msgRecv() {
return CQ::image("https://zhamao.xin/file/hello.jpg");
// 相当于返回:"[CQ:image,file=https://zhamao.xin/file/hello.jpg]"
}
}
```
效果
<chat-box>
) 发送图片
[ https://zhamao.xin/file/hello.jpg
</chat-box>
## CQ 码操作
### CQ::decode()
CQ 码字符反转义。
| 反转义前 | 反转义后 |
| -------- | -------- |
| `&amp;` | `&` |
| `&#91;` | `[` |
| `&#93;` | `]` |
```php
$str = CQ::decode("&#91;我只是一条普通的文本&#93;");
// 转换为 "[我只是一条普通的文本]"
```
### CQ::encode()
转义 CQ 码的敏感符号,防止 酷Q 把不该解析为 CQ 码的消息内容当作 CQ 码处理。
```php
$str = CQ::encode("[CQ:我只是一条普通的文本]");
// $str: "&#91;CQ:我只是一条普通的文本&#93;"
```
### CQ::removeCQ()
去除字符串中所有的 CQ 码。
```php
$str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]");
// $str: "这是带表情的全体消息"
```
## CQ 码列表
### CQ::face() - 发送 QQ 表情
发送 QQ 原生表情。
定义:`CQ::face($id)`
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [炸毛框架 1.x 版本文档](https://docs-v1.zhamao.xin/face_list.html)。
```php
/**
* @CQCommand("打盹")
*/
public function faceTest() {
ctx()->reply("正在打盹...");
ctx()->reply(CQ::face(8));
}
```
<chat-box>
) 打盹
( 正在打盹...
[ https://docs-v1.zhamao.xin/face/8.gif
</chat-box>
!!! note "提示"
对于不断更新的 QQ 版本下,可能会持续扩充新的 QQ 表情,如果上表没有新的表情的话,也可以使用消息接收的方式,让机器人收到表情后解析出来对应的 id 然后再发送。
### CQ::image() - 发送图片
发送图片。
定义:`image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1)`
参数
| 参数名 | 收 | 发 | 默认值 | 说明 |
| --------- | ---- | ---- | ------- | ------------------------------------------------------------ |
| `file` | ✓ | ✓ | 必填 | 图片文件名 |
| `flash` | ✓ | ✓ | `false` | 图片类型,当参数为 true 时代表发送闪照 |
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
发送时,`file` 参数除了支持使用收到的图片文件名直接发送外,还支持:
- 绝对路径,例如 `file:///root/imagetest/1.png`,格式使用 [`file` URI](https://tools.ietf.org/html/rfc8089)
- 网络 URL例如 `http://i1.piimg.com/567571/fdd6e7b6d93f1ef0.jpg`
- Base64 编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==`
### CQ::record() - 发送语音
发送语音消息。
定义:`CQ::record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1)`
参数
| 参数名 | 收 | 发 | 默认值 | 说明 |
| --------- | ---- | ---- | ------- | ------------------------------------------------------------ |
| `file` | ✓ | ✓ | 必填 | 音频文件名 |
| `flash` | ✓ | ✓ | `false` | 图片类型,当参数为 true 时代表发送闪照 |
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
发送时,`file` 参数除了支持使用收到的语音文件名直接发送外,还支持其它形式,参考上方发送图片。
```php
/**
* @CQCommand("说你好")
*/
public function say() {
ctx()->reply(CQ::record("https://zhamao.xin/file/hello.mp3"));
}
```
<chat-box>
) 说你好
( [语音消息,点击收听] 2'' )))
</chat-box>
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::at() - 群里@某人或全体
在群里 at 某个人或全体成员(全体成员需要有管理员权限)。
定义:`CQ::at($qq)`
参数:`$qq` 参数必填,如果填的是 QQ 号,则是单独 at 某人,如果是 `all`,则是 at 全体成员。
```php
/**
* @CQCommand("at测试")
*/
public function atTest() {
ctx()->reply(CQ::at(627577391)." 你好啊!");
}
```
<chat-box>
) at测试
( @鲸鱼 你好啊!
</chat-box>
### CQ::video() - 发送短视频
发送短视频。
定义:`CQ::video($file, $cache = true, $proxy = true, $timeout = -1)`
参数
| 参数名 | 收 | 发 | 默认值 | 说明 |
| --------- | ---- | ---- | ------ | ------------------------------------------------------------ |
| `file` | ✓ | ✓ | 必填 | 短视频文件名 |
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
发送时,`file` 参数除了支持使用收到的视频文件名直接发送外,还支持其它形式,参考上方发送图片。
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::rps() - 猜拳
定义:`CQ::rps()`
用法:`CQ::rps()`
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::dice() - 掷骰子
定义:`CQ::dice()`
用法:`CQ::dice()`
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::shake() - 窗口抖动
定义:`CQ::shake()`
用法:`CQ::shake()`
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::poke() - 戳一戳
发送戳一戳。
定义:`CQ::poke($type, $id, $name = "")`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| ------ | ---- | ---- | ------------------------------------------------------------ | ------ |
| `type` | ✓ | ✓ | 见 [Mirai 的 PokeMessage 类](https://github.com/mamoe/mirai/blob/f5eefae7ecee84d18a66afce3f89b89fe1584b78/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt#L49) | 类型 |
| `id` | ✓ | ✓ | 同上 | ID |
| `name` | ✓ | | 同上 | 表情名 |
例子:`CQ::poke(6,-1)`
效果:放大招
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::anonymous() - 匿名发消息
匿名发消息。需要在允许匿名发消息的群里发。
!!! tip "提示"
当收到匿名消息时,需要通过 [消息事件的群消息](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#群消息) 的 `anonymous` 字段判断。
定义:`CQ::anonymous($ignore = 1)`
```php
/**
* @CQCommand("匿名测试")
*/
public function anonymousTest() {
ctx()->reply(CQ::anonymous()."匿名测试");
}
```
### CQ::share() - 链接分享
发送链接分享卡片,可自定义内容。
定义:`CQ::share($url, $title, $content = null, $image = null)`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| --------- | ---- | ---- | -------- | -------------- |
| `url` | ✓ | ✓ | - | URL |
| `title` | ✓ | ✓ | - | 标题 |
| `content` | ✓ | ✓ | - | 可选,内容描述 |
| `image` | ✓ | ✓ | - | 可选,图片 URL |
```php
/**
* @CQCommand("链接分享测试")
*/
public function shareTest() {
ctx()->reply(CQ::share("https://baidu.com", "UC忽悠部", "震惊!我市一男子在光天化日之下..."));
}
```
### CQ::contact() - 推荐好友
发送推荐好友的卡片。
定义:`CQ::contact($type, $id)`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| ------ | ---- | ---- | ------------- | ---------------------- |
| `type` | ✓ | ✓ | `qq``group` | 推荐好友或群 |
| `id` | ✓ | ✓ | - | 被推荐人的 QQ 号或群号 |
```php
/**
* @CQCommand("我的名片")
*/
public function myCard() {
ctx()->reply(CQ::contact("qq", ctx()->getUserId()));
}
```
### CQ::location() - 发送位置
发送位置,基于经纬度坐标发的。
定义:`CQ::location($lat, $lon, $title = "", $content = "")`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| --------- | ---- | ---- | -------- | -------------- |
| `lat` | ✓ | ✓ | - | 纬度 |
| `lon` | ✓ | ✓ | - | 经度 |
| `title` | ✓ | ✓ | - | 可选,标题 |
| `content` | ✓ | ✓ | - | 可选,内容描述 |
### CQ::music() - 音乐分享
分享音乐,通过卡片。
发送音乐分享卡片。此 CQ 码如果伴随着其他文字,则文字内容会被丢弃。
定义:`CQ::music($type, $id_or_url, $audio = null, $title = null, $content = null, $image = null)`
- `$type`: 发送类型
- `$id_or_url`: 音乐的 id 或 音乐卡片点进去打开的链接
- `$audio`: 音频文件的 HTTP 地址
- `$title`: 音乐卡片的标题,建议 12 字以内
- `$content`: 音乐卡片的简介内容(可选)
- `$image`: 音乐卡片的图片的链接地址(可选)
如果 `$type` 参数为 `qq``163``xiami`,则必须且只和第二个参数 `$id_or_url` 配合使用。这三个为内置分享,需要先通过搜索功能获取对应平台歌曲的 id 后使用。
如果 `$type` 参数为 `custom`,则表明此音乐卡片为用户自定义,你可以根据自己的需要自定义卡片内容和音频。此时必须填写 `$id_or_url`, `$audio`, `$title` 三个参数。
```php
ctx()->reply(CQ::music("163", "730806")); //一首我喜欢的歌
// 以内置的发送类型发送音乐卡片,我这里挑了网易云音乐的一首歌。
ctx()->reply("custom", "https://baidu.com/", "https://zhamao.xin/file/hello.mp3", "我是Siri说出来的Hello", "不服来打我呀!", "https://zhamao.xin/file/hello.jpg");
// 自定义整个卡片的每个内容
```
### CQ::forward() - 合并转发
合并转发消息。
定义:`CQ::forward($id)`
参数
```
[CQ:forward,id=123456]
```
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| ------ | ---- | ---- | -------- | ------------------------------------------------------------ |
| `id` | ✓ | | 必填 | 合并转发 ID需通过 [`get_forward_msg` API](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-获取合并转发消息) 获取具体内容 |
### CQ::node() - 合并转发自定义节点
接收时,此消息段不会直接出现在消息事件的 `message` 中,需通过 [`get_forward_msg` API](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-获取合并转发消息) 获取。发送时,通过获取回来的 API 节点信息进行发送。
定义:`CQ::node($user_id, $nickname, $content)`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| ---------- | ---- | ---- | -------- | ------------------------------------------------------------ |
| `user_id` | ✓ | ✓ | - | 发送者 QQ 号 |
| `nickname` | ✓ | ✓ | - | 发送者昵称 |
| `content` | ✓ | ✓ | - | 消息内容,支持发送消息时的 `message` 数据类型,见 [API 的参数](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/#参数) |
```php
/**
* @CQCommand("node测试")
*/
public function nodeTest() {
ctx()->reply(CQ::node(123456, "Jack", "[CQ:face,id=123]哈喽~"));
}
```
### CQ::xml() - XML 消息
发送 QQ 兼容的 XML 多媒体消息。
定义:`CQ::xml($data)`
参数:`$data` 为 xml 字符串
```php
/**
* @CQCommand("xml测试")
*/
public function xmlTest() {
ctx()->reply(CQ::xml("<?xml ..."));
}
```
### CQ::json() - JSON 消息
发送 QQ 兼容的 JSON 多媒体消息。
定义:`CQ::json($data)`
参数同上,内含 JSON 字符串即可。
!!! tip "提示"
因为某些众所周知的原因XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。

View File

@@ -0,0 +1,231 @@
# 全局方法
全局方法就是 PHP 的全局函数,任意位置都可以调用,无需使用 use 字样。
## getClassPath()
根据加载的用户编写的代码类名来获取类所在的文件路径。
=== "src/Module/Example/Hello.php"
```php
<?php
namespace Module\Example;
class Hello { ... }
```
=== "src/Module/Example/Start.php"
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnStart;
class Start {
/**
* @OnStart()
*/
public function onStart() {
Console::info("Path: ".getClassPath(Hello::class));
}
}
```
=== "输出结果"
```
[11:12:02] [I] [#0] Path: /mnt/d/project/zhamao-framework/src/Module/Example/Hello.ph
```
## explodeMsg()
切割字符串的函数支持多空格换行tab。
定义:`explodeMsg($msg, $ban_comma = false)`
```php
$s = explodeMsg("你好啊 你好你好\n我还有多个空格 哈哈哈");
echo json_encode($s, 128|256); // ["你好啊","你好你好","我还有多个空格","哈哈哈"]
```
## unicode_decode()
Unicode 解码,一般用于被转义的 Unicode 转回来。
```php
echo unicode_decode("\u4f60\u597d"); // 你好
```
## matchPattern()
根据星号匹配字符串(非正则表达式)。
匹配示例:
- `你今天*了吗` -> 你今天喝水了吗
- `*的天气怎么样` -> 德州的天气怎么样
- `把*翻译成*` -> 把茶翻译成英语
定义:`matchPattern($pattern, $context)`
`$pattern` 为匹配模式,例如 `你今天*了吗`。
`$context` 为要判断是否匹配的内容。
返回值:`bool`,当为 true 时代表规则是匹配的false 代表不匹配。
```php
matchPattern("*是个啥?", "996是个啥"); // true
matchPattern("我想听*唱歌", "你想听谁唱歌"); // false
matchPattern("*把*翻译成*", "请把你好翻译成阿拉伯语"); // true
```
## split_explode()
和 `explodeMsg()` 类似,用作分割字符串,不过此函数加入了对 `中文|数字` 两者的分割,也就是说中文和数字之间也会被分割。
定义:`split_explode($del, $str, $divide_en = false)`
```php
split_explode(" ", "前进20 急啊急啊"); // ["前进","20","急啊急啊"]
```
`$del` 和 `explode()` 的第一个参数作用相同,作为初期分割的标志。
`$str` 表示待分割的内容。
`$divide_en` 表示是否分割中文和英文,如果为是,则中文和英文之间也会被分割开。
## matchArgs()
`matchPattern()` 的扩展,如果 `matchPattern()` 格式的字符串和模式匹配成功,则通过星号位置来提取星号匹配到的内容,参数同 `matchPattern()`。
```php
$r = matchArgs("把*翻译成*", "把日语翻译成英语"); // ["日语","英语"]
```
## connectIsQQ()
判断当前 WebSocket 连接是否为 OneBot 标准的机器人客户端。
## connectIsDefault()
判断连接是否是未定义类型的 WebSocket 连接。
## connectIs()
判断连接是否是对应类型的 WebSocket 连接。
```php
connectIs("your_another_type_connect");
```
## set_coroutine_params()
设置当前上下文中的一些变量。
```php
set_coroutine_params(["data" => [
"post_type" => "message",
...
]]);
```
## ctx()
别名:`context()`,获取当前协程的上下文,见 [上下文](/component/context/)。
## zm_debug()
同 `Console::debug($msg)`。
## zm_sleep()
协程版 `sleep()` 函数。
定义:`zm_sleep($s = 1)`
`$s`睡眠的时间可支持小数。例如0.001 代表 1 毫秒)
为什么不用 PHP 自带的 sleep 呢?因为炸毛框架是基于协程的,协程版 sleep 需要使用 Swoole 自带的 sleep。此函数做了一个简单的封装。
```php
zm_sleep(5);
zm_sleep(0.05);
```
## zm_exec()
执行系统命令,替代 PHP 的 `exec()`。
定义:`zm_exec($cmd)`
返回值:
```php
array(
'code' => 0, // 进程退出的状态码
'signal' => 0, // 信号
'output' => 'hello world', // 输出内容
);
```
```php
$result = zm_exec("echo 'hello world'")["output"];
```
## zm_cid()
获取当前协程的 ID效果等同于 `\Swoole\Coroutine::getCid()`。
## zm_yield()
挂起当前协程,直到手动恢复,效果等同于 `\Swoole\Coroutine::yield()`。
## zm_resume()
恢复继续执行协程,效果等同于 `\Swoole\Coroutine::resume()`。
```php
$r = 0;
function test() {
echo "hello-1\n";
global $r;
$r = zm_cid();
zm_yield();
echo "hello-2\n";
}
go("test");
echo "hello-3\n";
zm_resume($r);
```
输出结果:
```
hello-1
hello-3
hello-2
```
## server()
获取 Swoole Server 对象进行操作,效果等同于 `\ZM\Framework::$server`。
```php
echo server()->worker_id.PHP_EOL; // 0
```
## bot()
返回 ZMRobot 操作机器人 API 的对象。
对于默认的模式,如果框架连接了多个机器人实例,则会随机返回一个机器人的 API 实例。如果使用了单例模式,则返回单例模式的机器人 API 实例。
```php
bot()->sendPrivateMsg(123456, "你好啊!!");
// 等同于 ZMRobot::getRandom()->sendPrivateMsg(123456, "你好啊!!");
```

View File

@@ -1,3 +1,3 @@
# 框架组件
还没写到这里,不着急
这里列举了框架内的你可能会用到的常用组件。

View File

@@ -0,0 +1,320 @@
# LightCache 轻量缓存
在炸毛框架 1.x 时代,框架里有非常方便使用的 ZMBuf 缓存,但是由于 2.x 版本框架加入了多进程模式所以不能再以传统的存到全局变量的方式来构建和管理缓存了LightCache 就是替代方案。LightCache 依旧是 key-value 键值对形式的存储,支持多种类型的变量。
定义:`ZM\Store\LightCache`
## 与 ZMBuf 的不同
从存储内容角度LightCache 存入的是 Swoole 初始化的共享内存,基于 Swoole/Table 编写。优势在于多进程下的性能极佳,而且没有数据同步问题;劣势在于它需要在启动框架前就声明总大小,不能根据存储数据的大小来划定,需提前指定最大能存储的容量。而 ZMBuf 基于直接把变量存到静态成员中 `public static $data` 类似这样,且 1.x 框架基于单进程单线程,无任何数据同步的问题。
总之来说LightCache 是让用户在涉及多进程编程时,一个折中的解决方案,提出和解决了很多多进程开发时存储数据遇到的问题:数据同步、进程间通信效率、数据是否需要上锁等。
- 数据同步:多进程下因为是固定的内存大小区域,所以每个进程读取和写入都是只有一份数据的,不存在数据不同步的问题。
- 进程间通信:因为多个进程共享一片区域的内存,所以不需要进程间通信,无协程切换。
- 镀锡是否需要上锁:看情况。一般情况下 Swoole/Table 模块自带一个行锁,只有两个进程在两个 CPU 上同时读取一行数据时才会发生抢锁,作为框架的使用者,如果只写或只读,是无需手动上任何锁的。只有在先 `get()``set()` 这样的情况才需要上自旋锁。后面的段会详细讲述。
使用体验上,基本和 ZMBuf 无差,如果没有用过 1.x 的版本,可无视此段话。
## 使用
### 配置和初始化
配置文件还是在 `config/global.php` 文件里,字段是 `light_cache`
```php
/** 轻量字符串缓存,默认开启 */
$config['light_cache'] = [
'size' => 1024, //最多允许储存的条数需要2的倍数
'max_strlen' => 16384, //单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'].'_cache.json',
'auto_save_interval' => 900
];
```
其中 `$size` 是最多保存的键值对数目,填写非 2 的倍数时底层会自动修正为 2 的倍数值。
`$max_strlen` 为单条值最长保存的长度。因为 Swoole/Table 只能存数字、字符串所以在存取数组等变量时会先将其序列化为字符串形式保存get 时自动反序列化回来。在存数组等非字符串变量时,请先自行计算你要存取的内容序列化后的最大长度。如果长度超出最大长度,则无法保存,`set()` 将返回 false。
`hash_conflict_proportion`Table 模块底层使用 hash 表,会存在 hash 冲突,调大 Hash 冲突率会提升 `size` 指定条目数的准确性,但也将增加物理内存的使用。这里单位是百分比,`0.6``60%`
`persistence_path` 是持久化保存变量的文件保存位置,默认在 `zm_data/_cache.json` 文件。
`auto_save_interval` 是持久化保存变量的自动保存时间,单位秒。
### LightCache::set()
设置内容。
定义:`LightCache::set($key, $value, $expire = -1)`
返回值:`bool`。当 value 超出了最大长度或内存不足时,返回 false其余 true。
参数:
`$key` 的长度不能超过 64 字节,且不能存入二进制内容。
`$value` 可存入 `bool``string``int``array` 等可被 `json_encode()` 的变量,闭包函数和对象不可存入。
`$expire``int`,超时时间(秒)。如果设定了大于 0 的值,则表明是在 `$expire` 秒后自动删除。如果为 -1 则什么都不做,如果框架使用了 `stop` 或 Ctrl+C 或意外退出时数据会丢失。如果为 -2则会将此数据持久化保存保存在上方配置文件指定的 json 文件中,待关闭后再次启动框架会自动加载回来,不会丢失。
```php
// use ZM\Store\LightCache;
/**
* @CQCommand("store")
*/
public function store() {
LightCache::set("key1", ["value1" => "strOrInt", "value2" => 123]);
return "OK!";
}
/**
* @CQCommand("storeAfterRemove")
*/
public function storeAfterRemove() {
LightCache::set("store1", "remove1", 30);
ctx()->reply(LightCache::get("store1") !== null ? "内容存在!" : "内容不存在!");
zm_sleep(30);
ctx()->reply(LightCache::get("store1") !== null ? "内容存在!" : "内容不存在!");
}
```
<chat-box>
) store
( OK
) storeAfterRemove
( 内容存在!
^ 等待 30 秒
( 内容不存在!
</chat-box>
### LightCache::get()
获取内容。
返回值:`mixed|null`。当无内容或过期时返回 null剩余情况返回原数据。
### LightCache::getExpire()
获取存储项剩余过期时间(秒)。
定义:`LightCache::getExpire(string $key)`
```php
$s = LightCache::set("test", "hello", 20);
zm_sleep(10);
dump(LightCache::getExpire("test")); // 返回 10
```
### LightCache::getMemoryUsage()
获取轻量缓存使用的总空间大小(字节)
```php
LightCache::getMemoryUsage());
```
轻量缓存的内存手工计算方式:(Table 结构体长度` + `KEY 长度 64 字节 + `$size`) * (1 + `$conflict_proportion`) * 列尺寸。
Table 结构体长度根据你所设定的 `max_strlen` 会变化。
> 框架默认配置下的轻量缓存启动后大约占用内存 25MB 左右。
### LightCache::isset()
判断某项是否存在。
```php
LightCache::set("foo", "bar");
dump(LightCache::isset("foo")); // true
```
### LightCache::unset()
删除某项。
```php
LightCache::set("foo", "bar");
LightCache::unset("foo");
dump(LightCache::isset("foo")); // false
```
### LightCache::getAll()
获取所有项。
```php
LightCache::set("k1", ["I", "am", "array"]);
LightCache::set("k2", "v2");
LightCache::set("k3", 20001);
dump(LightCache::getAll());
/*
{
"k1": ["I", "am", "array"],
"k2": "v2",
"k3": 20001
}
*/
```
### LightCache::savePersistence()
立刻保存所有被标记为持久化的缓存项到磁盘。
!!! note "提示"
在一般情况下框架定时执行此方法来保存在停止框架、reload 框架和 Ctrl+C 停止框架的时候,均会执行保存。
### 持久化
`set()` 的 expire 设置为 -2 即可。
```php
/**
* @CQCommand("store")
*/
public function store() {
LightCache::set("msg_time", time(), -2);
return "OK!";
}
/**
* @CQCommand("getStore")
*/
public function getStore() {
return "存储时间:".date("Y-m-d H:i:s", LightCache::get("msg_time"));
}
```
<chat-box>
^ 我在 2021-01-05 15:21:00 发送这条消息
) store
( OK!
^ 这时我用 Ctrl+C 停止框架,过一会儿再启动
) getStore
( 存储时间2021-01-05 15:21:00
</chat-box>
### 数据加锁
在特定情况下,使用 LightCache 必须配合锁使用,否则会出现数据错乱。我们来看下面的例子:
```php
/**
* @RequestMapping("/test")
*/
public function test() {
$s = LightCache::get("web_count");
if($s === null) $s = 1;
else $s += 1;
LightCache::set("web_count", $s);
return "<h1>It works!</h1>";
}
```
我们使用压测工具,例如 `ab`,对此路由接口开很多很多线程进行测试,假设我们设置请求总数为 200000 次,框架的工作进程数为 8我用的是 2020 年末的 i5 MacBook Pro 13 inch
> 懒得再测了,下面就口述过程吧。
在运行完测试后,通过 `LightCache::get("web_count")`,获取到的数你会发现不是 200000。怎么回事呢请自行翻阅多进程开发相关的书籍哦或者简单理解为有一些情况下进程 1 执行到了 `if-else` 语句,另一个进程也执行到了这里,两次在代码层面加的数是相同的,则虽然请求了两次,但是后执行 set 的那个进程又覆盖了前一个进程执行的值,导致最终结果加了 1 而不是 2
!!! note "提示"
同样的场景,使用 ZMAtomic 就不需要使用锁了。Atomic 是一句话:`add(1)` 立即加值的。而 LightCache 需要加锁的情况一般都是 `get->改值->set` 这样的代码。
解决这一问题,就需要用到锁。这种情况下,我们首先考虑的是自旋锁,框架也因此内置了一个方便使用的自旋锁组件。详见下一章:自旋锁。
## 如何临时缓存大变量
由于 LightCache 需要提前声明最大大小,所以在某些情况下,比如第三方 API 接口结果临时缓存,可能不太适合使用,这时对于 2.x 版本的多进程炸毛框架是一个新的问题。
解决方案有三种:
-`global.php` 中的 `swoole.worker_num` 调整为 `1` 即可,所有除所有主 handler 事件的用户类外其他类均可使用如 `Hello::$store` 类似的静态变量全局存取
- 使用 WorkerCache需要 2.2.0 以上版本)
- 使用 Redis需要安装 `redis` 扩展)
以上WorkerCache 是为了弥补 LightCache 的不足而诞生的,以下就是 WorkerCache 的具体内容。
### WorkerCache 跨进程大缓存
WorkerCache 和 LightCache 几乎完全不同WorkerCache 存储的方式说白了就是 PHP 的静态变量,不过框架支持使用封装好的进程间通信进行跨进程读取。但由于需要设置一个存储变量的进程,所以配置文件必须先指定要将数据存到哪个 Worker/TaskWorker 进程中。关于框架内多进程的说明,请见 [进阶 - 多进程 Hack](/advanced/multi-process/)。
定义:`ZM\Store\WorkerCache`
#### 配置
见 [基本配置](/guide/basic-config/)。
#### WorkerCache::get()
定义:`get($key)`
`$key` 为指定要获取的键值对的值,如果不存在则返回 null。
#### WorkerCache::set()
定义:`set($key, $value, $async = false)`
设置变量,你懂的。
注意,`$value` 可以是被无损 `json_encode``json_decode` 的变量闭包Closure、资源resource等类型不支持存储。
`$async` 默认为 false当为 true 时候,不会返回是否成功设置与否,否则会协程等待是否目标进程存储成功。
#### WorkerCache::unset()
定义:`unset($key, $async = false)`
删除键对应的值。`$async` 的意义同上。
#### WorkerCache::add()
定义:`add($key, int $value, $async = false)`
给 int 类型的值加一,如果值不存在,则默认为 0 且加上目标的 `$value`
#### WorkerCache::sub()
定义:`sub($key, int $value, $async = false)`
给 int 类型的值减一,如果值不存在,则默认为 0 且减去目标的 `$value`
```php
<?php
namespace Module\Example;
use ZM\Store\WorkerCache;
use ZM\Annotation\CQ\CQCommand;
class Hello {
/**
* @CQCommand("set_store")
*/
public function setStorage() {
$arg1 = ctx()->getFullArg("请输入要设置的内容名称");
$arg2 = ctx()->getFullArg("请输入要设置的内容");
WorkerCache::set($arg1, $arg2);
return "成功!";
}
/**
* @CQCommand("get_store")
*/
public function getStorage() {
$arg1 = ctx()->getFullArg("请输入要获取的内容名称");
$data = WorkerCache::get($arg1);
return $data ?? "内容不存在!";
}
}
```
<chat-box>
) set_store hello world
( 成功!
) get_store hello
( world
) get_store foo
( 内容不存在!
</chat-box>

97
docs/component/mysql.md Normal file
View File

@@ -0,0 +1,97 @@
# MySQL 数据库
## 配置
炸毛框架的数据库组件支持原生 SQL、查询构造器去掉了复杂的对象模型关联同时默认为数据库连接池使开发变得简单。
数据库的配置位于 `config/global.php` 文件的 `sql_config` 段。
数据库操作的唯一核心工具类和功能类为 `\ZM\DB\DB`,使用前需要配置 host 和 use 此类。
## 查询构造器
在 炸毛框架 中,数据库查询构造器为创建和执行数据库查询提供了一个方便的接口,它可用于执行应用程序中大部分数据库操作。同时,查询构造器使用 `prepare` 预处理来保护程序免受 SQL 注入攻击,因此没有必要转义任何传入的字符串。
### 新增数据
```php
DB::table("admin")->insert(['admin_name', 'admin_password'])->save();
// INSERT INTO admin VALUES ('admin_name', 'admin_password')
```
其中 `insert` 的参数是插入条目的数据列表。假设 admin 表有 `name``password` 两列。
> 自增 ID 插入 0 即可。
### 删除数据
```php
DB::table("admin")->delete()->where("name", "admin_name")->save();
// DELETE FROM admin WHERE name = 'admin_name'
```
其中 `where` 语句的第一个参数为列名,当只有两个参数时,第二个参数为值,效果等同于 SQL 语句:`WHERE name = 'admin_name'`,当含有第三个参数且第二个参数为 `=``!=``LIKE` 的时候,效果就是 `WHERE 第一个参数 第二个参数的操作符 第三个参数`
### 更新数据
```php
DB::table("admin")->update(["name" => "fake_admin"])->where("name", "admin_name")->save();
// UPDATE admin SET name = 'fake_admin' WHERE name = 'admin_name'
```
`update()` 方法中是要 SET 的内容的键值对,例如上面把 `name` 列的值改为 `fake_admin`
### 查询数据
```php
$r = DB::table("admin")->select(["name"])->where("name", "fake_admin")->fetchFirst();
// SELECT name FROM admin WHERE name = 'fake_admin'
echo $r["name"];
echo DB::table("admin")->where("name", "fake_admin")->value("name");
// SELECT * FROM admin WHERE name = 'fake_admin'
```
`select()` 方法的参数是要查询的列,默认留空为 `["*"]`,也就是所有列都获取,也可以在 table 后直接 where 查询。
其中 `fetchFirst()` 获取第一行数据。
如果只需获取一行中的一个字段值,也可以通过上面所示的 `value()` 方法直接获取。
多列数据获取需要使用 `fetchAll()`
```php
$r = DB::table("admin")->select()->fetchAll();
// SELECT * FROM admin WHERE 1
foreach($r as $k => $v) {
echo $v["name"].PHP_EOL;
}
```
### 查询条数
```php
DB::table("admin")->where("name", "fake_admin")->count();
//SELECT count(*) FROM admin WHERE name = 'fake_admin'
```
## 直接执行 SQL
> 在查询器外执行的 SQL 语句都不会被缓存,都是一定会请求数据库的。
### DB::rawQuery()
- 用途:直接执行模板查询的裸 SQL 语句。
- 参数:`$line``$params`
- 返回:查到的行的数组
`$line` 为请求的 SQL 语句,`$params` 为模板参数。
```php
$r = DB::rawQuery("SELECT * FROM admin WHERE name = ?", ["fake_admin"]);
//SELECT * FROM admin WHERE name = 'fake_admin'
echo $r[0]["password"];
```
> 参数查询已经从根本上杜绝了 SQL 注入的问题。

62
docs/component/redis.md Normal file
View File

@@ -0,0 +1,62 @@
# Redis
炸毛框架内置了 Redis 连接池,供开发者使用。使用前需要先安装 `redis` 扩展:
```bash
pecl install redis
```
> 如果是 Docker 环境,则默认已安装。
## 配置
配置文件在 `config/global.php` 的全局配置文件下,详情见 [配置](/guide/basic-config/#redis_config)。
示例配置(假设 Redis Server 开到了本地):
```php
/** Redis连接信息host留空则启动时不创建Redis连接池 */
$config['redis_config'] = [
'host' => '127.0.0.1',
'port' => 6379,
'timeout' => 1,
'db_index' => 0,
'auth' => ''
];
```
## 使用
当写好配置文件后,不可以使用 reload 进行重载,因为连接池需要在主进程中声明配置,才可以应用到多个工作进程中。所以必须输入 `stop` 或 Ctrl+C 停止后再启动框架。
定义:`ZM\Store\Redis\ZMRedis`
因为使用的是连接池,所以每次使用完一个连接需要归还连接给连接池。框架封装了两种方式自动归还,你可以选择下面其中的任意一种。
以下的方式获取的 `$redis` 都是 `redis` 扩展的对象 `\Redis`,关于 redis 扩展的方法文档,详情见:[Redis 文档](https://www.php.cn/course/49.html)。
### 对象模式
```php
$obj = new ZMRedis();
$redis = $obj->get();
ctx()->reply($redis->ping("123"));
```
### 回调模式
```php
// 前面的代码
ZMRedis::call(function($redis) {
$redis->set("key1", "hello world");
$result = $redis->get("key1");
ctx()->reply($result);
});
// 后面的代码
```
### 二者的区别
选一个喜欢的就好。硬要是说区别的话,对象模式是在 PHP 自动回收这个 `ZMRedis` 对象时会归还连接,也可以通过手动 `unset($obj)` 进行回收,否则就会执行到函数结尾自动回收。切记不可将 `$obj` 对象持久化存到静态或全局变量等。
回调模式看似是回调,但是是同步执行的,不会发生顺序错乱。也就是说到了 `ZMRedis::call()` 方法里面的时候,后面的代码不会提前执行,是顺序执行的。回调的作用仅仅是用作自动回收连接对象。

790
docs/component/robot-api.md Normal file
View File

@@ -0,0 +1,790 @@
# 机器人 APIZMRobot
ZMRobot 类是封装好的 OneBot 标准的 API 接口调用类,可以在机器人连接后通过连接或者机器人 QQ 号获取对象并调用接口(如发送群消息、获取群列表等操作)。
| 属性项 | 属性值 | 备注 |
| -------- | ---------------- | ------------------------------ |
| 名称 | ZMRobot | |
| 类型 | 实例化类 | `$r = new ZMRobot($conn)` |
| 命名空间 | `ZM\API\ZMRobot` | 使用前先 `use ZM\API\ZMRobot;` |
## 属性
对象属性方法是对 API 的调整,例如是否以 `_async``_rate_limited` 后缀发送 API、设置协程返回还是异步返回结果等。
### ZMRobot::API_NORMAL
以默认(无后缀)方式请求 API。
### ZMRobot::API_ASYNC
以后缀 `_async` 方式异步请求 API。
### ZMRobot::API_RATE_LIMITED
以后缀 `_rate_limited` 方式请求 API。
## 方法
### setPrefix()
设置后缀。目前支持 `_async``_rate_limited`
- **prefix**: `int` `默认:API_NORMAL`,可选 `ZMRobot::API_NORMAL``ZMRobot::API_ASYNC``ZMRobot::API_RATE_LIMITED`
设置后缀后,请求的 API 会发生变化。例如发送私聊消息:`sendPrivateMsg()`,请求的 API 为 `send_private_msg_async`,详见 [OneBot 文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/README.md)。
### setCallback()
设置 API 结果返回方式。默认为 true就是直接通过框架处理后接收回包直接返回给结果。如果为 false则 API 请求后只返回是否成功推送出 WS 数据包。
### getSelfId()
获取当前对象的机器人 QQ 或 OneBot 实例的 ID。
```php
$bot = ZMRobot::get(123456);
echo $bot->getSelfId(); //123456
```
### ZMRobot::get()
静态方法,用来通过机器人 QQ 或 OneBot 实例的 ID 获取 ZMRobot 对象。
参数:`$robot_id`,必填。
```php
$r = ZMRobot::get(123456);
$r->sendPrivateMsg(55555, "hello");
```
### ZMRobot::getRandom()
静态方法,随机获取一个连接到框架的机器人(多个机器人实例连接到框架时适用)。
如果框架没有连接到任何机器人实例,则会抛出一个异常:`ZM\Exception\RobotNotFoundException`
```php
try {
$bot = ZMRobot::getRandom();
$bot->sendPrivateMsg(55555, "foo");
} catch (\ZM\Exception\RobotNotFoundException $e) {
echo "还没有机器人连接到框架!\n";
}
```
### ZMRobot::getAllRobot()
获取所有连接到框架的机器人的 ZMRobot 对象。
返回值:`ZMRobot[]`
```php
$all = ZMRobot::getAllRobot();
foreach($all as $v) {
$v->sendPrivateMsg(55555, "机器人轮流给一个人发消息啦!");
}
```
### __construct()
构造方法。
参数:`$connection`:炸毛框架内部的连接对象,必填参数。
```php
//从上下文获取 Websocket 连接对象
$conn = ctx()->getConnection();
$bot = new ZMRobot($conn);
```
## 返回结果处理
因为框架的机器人是兼容 OneBot 标准的(原 CQHTTP所以每次接收发送 API 请求的结果都是大体一样的结构。我们以 `sendPrivateMsg()` 为例,因为发送出去的每一条消息都会在 OneBot 实例(如 CQHTTP 插件、go-cqhttp 等)中对应一个消息 ID以供我们核查消息和后续撤回等操作需要。
```php
$bot = ZMRobot::get("123456"); // 机器人QQ号
$obj = $bot->sendGroupMsg("234567", "你好");
echo json_encode($obj, 128|256);
```
```json
// 输出结果
{
"status": "ok",
"retcode": 0,
"data": {
"message_id": 1243
}
}
```
如上,`$obj` 就是我们的回包内容,我们通过调用 `sendPrivateMsg()` 这个 API 后,会拿到机器人发送此条消息的消息 ID然后可以通过它来进行其他操作例如撤回
```php
$result = $bot->deleteMsg($obj["data"]["message_id"]);
vardump($result["retcode"]); //如果成功撤回,输出 int(0)
```
### 状态码和 Data
状态码一般情况成功都是 0 或者 200在过去炸毛框架兼容 CQHTTP 插件时,错误码的标准按照 CYKU 给出的标准编写,不同的 OneBot 标准的实现,可能有不同的数值,需要根据你对接的机器人客户端进行适配。
结果中返回的 `data` 字段根据下方不同 API 的调用而不同,具体查看每个 API 写明的 `响应数据` 表格。
### response 表
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------- | -------- | ---------- | ----------------------- |
| `status` | String | "ok" | 状态码说明 |
| `retcode` | number | 0 | 返回状态码 |
| `data` | array | 见 data 表 | 根据不同的 API 返回不同 |
## 机器人 API 方法
### sendPrivateMsg()
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------- | -------- | ------- | ------------------------------------------------------------ |
| `user_id` | number | - | 对方 QQ 号 |
| `message` | message | - | 要发送的内容 |
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------- |
| `message_id` | number (int32) | 消息 ID |
例子
=== "代码"
```php
$bot = ZMRobot::get(123456); // 123456是你的机器人QQ
$bot->sendPrivateMsg("627577391", "你好啊!你好你好!");
```
=== "效果"
<chat-box>
( 你好啊!你好你好!
</chat-box>
### sendGroupMsg()
发送群组消息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------- | -------- | ------- | ------------------------------------------------------------ |
| `group_id` | number | - | 群号 |
| `message` | message | - | 要发送的内容 |
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------- |
| `message_id` | number (int32) | 消息 ID |
### sendMsg()
发送消息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| -------------- | -------- | ------- | ------------------------------------------------------------ |
| `message_type` | string | - | 消息类型,支持 `private`、`group`、`discuss`,分别对应私聊、群组、讨论组,如不传入,则根据传入的 `*_id` 参数判断 |
| `target_id` | number | - | 目标号码,如 QQ 号,群号,讨论组号 |
| `message` | message | - | 要发送的内容 |
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------- |
| `message_id` | number (int32) | 消息 ID |
### deleteMsg()
撤回消息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------ | -------------- | ------ | ------- |
| `message_id` | number (int32) | - | 消息 ID |
响应数据:无
### getMsg()
获取消息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------- | -------- | ------ | -------------------------------- |
| `message_id` | number | - | 消息 ID |
响应数据
| 字段名 | 数据类型 | 说明 |
| -------------- | -------------- | ------------------------------------------------------------ |
| `time` | number (int32) | 发送时间 |
| `message_type` | string | 消息类型,同 [消息事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md) |
| `message_id` | number (int32) | 消息 ID |
| `real_id` | number (int32) | 消息真实 ID |
| `sender` | object | 发送人信息,同 [消息事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md) |
| `message` | message | 消息内容 |
### getForwardMsg()
获取合并转发消息。
参数
| 字段名 | 数据类型 | 说明 |
| ------ | -------- | ----------- |
| `id` | string | 合并转发 ID |
响应数据
| 字段名 | 类型 | 说明 |
| --------- | ------- | ------------------------------------------------------------ |
| `message` | message | 消息内容,使用 [消息的数组格式](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/array.md) 表示,数组中的消息段全部为 [`node` 消息段](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#合并转发自定义节点) |
### sendLike()
发送好友赞。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------- | -------- | ------ | -------------------------------- |
| `user_id` | number | - | 对方 QQ 号 |
| `times` | number | 1 | 赞的次数,每个好友每天最多 10 次 |
响应数据
### setGroupKick()
群组踢人。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| -------------------- | -------- | ------- | ------------------ |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要踢的 QQ 号 |
| `reject_add_request` | boolean | `false` | 拒绝此人的加群请求 |
响应数据:无
### setGroupBan()
群组单人禁言。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | --------- | -------------------------------- |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要禁言的 QQ 号 |
| `duration` | number | `30 * 60` | 禁言时长单位秒0 表示取消禁言 |
响应数据:无
### setGroupAnonymousBan()
群组匿名用户禁言。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------------- | ---------------- | --------- | ------------------------------------------------------------ |
| `group_id` | number | - | 群号 |
| `anonymous_or_flag` | object 或 string | - | 要禁言的匿名用户对象(群消息上报的 `anonymous` 字段)或用户的 flag |
| `duration` | number | `30 * 60` | 禁言时长,单位秒,无法取消匿名用户禁言 |
上面的 `anonymous_or_flag` 两者任选其一传入即可。
响应数据:无
### setGroupWholeBan()
群组全员禁言
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | -------- |
| `group_id` | number | - | 群号 |
| `enable` | boolean | `true` | 是否禁言 |
响应数据:无
### setGroupAdmin()
群组设置管理员
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ------------------------- |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要设置管理员的 QQ 号 |
| `enable` | boolean | `true` | true 为设置false 为取消 |
响应数据:无
### setGroupAnonymous()
群组匿名
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ---------------- |
| `group_id` | number | - | 群号 |
| `enable` | boolean | `true` | 是否允许匿名聊天 |
响应数据:无
### setGroupCard()
设置群名片(群备注)
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ---------------------------------------- |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要设置的 QQ 号 |
| `card` | string | 空 | 群名片内容,不填或空字符串表示删除群名片 |
响应数据:无
### setGroupName()
设置群名。
参数
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------ |
| `group_id` | number (int64) | 群号 |
| `group_name` | string | 新群名 |
响应数据:无
### setGroupLeave()
退出群组
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------ | -------- | ------- | -------------------------------------------------------- |
| `group_id` | number | - | 群号 |
| `is_dismiss` | boolean | `false` | 是否解散,如果登录号是群主,则仅在此项为 true 时能够解散 |
响应数据:无
### setGroupSpecialTitle()
设置群组专属头衔
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------------- | -------- | ------ | ------------------------------------------------------------ |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要设置的 QQ 号 |
| `special_title` | string | 空 | 专属头衔,不填或空字符串表示删除专属头衔 |
| `duration` | number | `-1` | 专属头衔有效期,单位秒,-1 表示永久,不过此项似乎没有效果,可能是只有某些特殊的时间长度有效,有待测试 |
响应数据:无
### setFriendAddRequest()
处理加好友请求
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------- | -------- | ------ | ----------------------------------------- |
| `flag` | string | - | 加好友请求的 flag需从上报的数据中获得 |
| `approve` | boolean | `true` | 是否同意请求 |
| `remark` | string | 空 | 添加后的好友备注(仅在同意时有效) |
响应数据:无
### setGroupAddRequest()
处理加群请求 / 邀请
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ------------------------------------------------------------ |
| `flag` | string | - | 加群请求的 flag需从上报的数据中获得 |
| `sub_type` | string | - | `add` 或 `invite`,请求类型(需要和上报消息中的 `sub_type` 字段相符) |
| `approve` | boolean | `true` | 是否同意请求/邀请 |
| `reason` | string | 空 | 拒绝理由(仅在拒绝时有效) |
响应数据无
### getLoginInfo()
获取登录号信息
参数:无
响应数据
| 字段名 | 数据类型 | 说明 |
| ---------- | -------------- | ------- |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | QQ 昵称 |
### getStrangerInfo()
获取陌生人信息
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `user_id` | number | - | QQ 号 |
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
响应数据
| 字段名 | 数据类型 | 说明 |
| ---------- | -------------- | ------------------------------------- |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `sex` | string | 性别,`male` 或 `female` 或 `unknown` |
| `age` | number (int32) | 年龄 |
### getFriendList()
获取好友列表
参数:无
响应数据
响应内容为 JSON 数组,每个元素如下:
| 字段名 | 数据类型 | 说明 |
| ---------- | -------------- | ------ |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `remark` | string | 备注名 |
### getGroupInfo()
获取群信息
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `group_id` | number | - | 群号 |
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------------ | -------------- | -------------------- |
| `group_id` | number (int64) | 群号 |
| `group_name` | string | 群名称 |
| `member_count` | number (int32) | 成员数 |
| `max_member_count` | number (int32) | 最大成员数(群容量) |
### getGroupList()
获取群列表
参数:无
响应数据
响应内容为 JSON 数组,每个元素如下:
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------ |
| `group_id` | number (int64) | 群号 |
| `group_name` | string | 群名称 |
### getGroupMemberInfo()
获取群成员信息
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | QQ 号 |
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------------- | -------------- | ------------------------------------- |
| `group_id` | number (int64) | 群号 |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `card` | string | 群名片/备注 |
| `sex` | string | 性别,`male` 或 `female` 或 `unknown` |
| `age` | number (int32) | 年龄 |
| `area` | string | 地区 |
| `join_time` | number (int32) | 加群时间戳 |
| `last_sent_time` | number (int32) | 最后发言时间戳 |
| `level` | string | 成员等级 |
| `role` | string | 角色,`owner` 或 `admin` 或 `member` |
| `unfriendly` | boolean | 是否不良记录成员 |
| `title` | string | 专属头衔 |
| `title_expire_time` | number (int32) | 专属头衔过期时间戳 |
| `card_changeable` | boolean | 是否允许修改群名片 |
### getGroupMemberList()
获取群成员列表
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ---- |
| `group_id` | number | - | 群号 |
响应数据
响应内容为 JSON 数组,每个元素的内容和上面的 `/get_group_member_info` 接口相同,但对于同一个群组的同一个成员,获取列表时和获取单独的成员信息时,某些字段可能有所不同,例如 `area`、`title` 等字段在获取列表时无法获得,具体应以单独的成员信息为准。
### getGroupHonorInfo()
获取群荣誉信息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------------- | ------ | ------------------------------------------------------------ |
| `group_id` | number (int64) | - | 群号 |
| `type` | string | - | 要获取的群荣誉类型,可传入 `talkative` `performer` `legend` `strong_newbie` `emotion` 以分别获取单个类型的群荣誉数据,或传入 `all` 获取所有数据 |
响应数据
| 字段名 | 数据类型 | 说明 |
| -------------------- | -------------- | ---------------------------------------------------------- |
| `group_id` | number (int64) | 群号 |
| `current_talkative` | object | 当前龙王,仅 `type` 为 `talkative` 或 `all` 时有数据 |
| `talkative_list` | array | 历史龙王,仅 `type` 为 `talkative` 或 `all` 时有数据 |
| `performer_list` | array | 群聊之火,仅 `type` 为 `performer` 或 `all` 时有数据 |
| `legend_list` | array | 群聊炽焰,仅 `type` 为 `legend` 或 `all` 时有数据 |
| `strong_newbie_list` | array | 冒尖小春笋,仅 `type` 为 `strong_newbie` 或 `all` 时有数据 |
| `emotion_list` | array | 快乐之源,仅 `type` 为 `emotion` 或 `all` 时有数据 |
其中 `current_talkative` 字段的内容如下:
| 字段名 | 数据类型 | 说明 |
| ----------- | -------------- | -------- |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `avatar` | string | 头像 URL |
| `day_count` | number (int32) | 持续天数 |
其它各 `*_list` 的每个元素是一个 JSON 对象,内容如下:
| 字段名 | 数据类型 | 说明 |
| ------------- | -------------- | -------- |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `avatar` | string | 头像 URL |
| `description` | string | 荣誉描述 |
### getCookies()
获取 Cookies。
!!! warning "注意"
目前开源的 mirai 为底层的机器人客户端均不支持获取 Cookies 和 CSRF Token包括 go-cqhttp。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| -------- | -------- | ------ | ----------------------- |
| `domain` | string | 空 | 需要获取 cookies 的域名 |
响应数据
| 字段名 | 数据类型 | 说明 |
| --------- | -------- | ------- |
| `cookies` | string | Cookies |
### getCsrfToken()
获取 CSRF Token
参数:无
响应数据
| 字段名 | 数据类型 | 说明 |
| ------- | -------------- | ---------- |
| `token` | number (int32) | CSRF Token |
### getCredentials()
获取 QQ 相关接口凭证,即上面两个合并。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| -------- | -------- | ------ | ----------------------- |
| `domain` | string | 空 | 需要获取 cookies 的域名 |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ---------- |
| `cookies` | string | Cookies |
| `csrf_token` | number (int32) | CSRF Token |
### getRecord()
获取语音。其实并不是真的获取语音,而是转换语音到指定的格式。
> **提示**:要使用此接口,通常需要安装 ffmpeg请参考 OneBot 实现的相关说明。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------ | -------- | ------ | ------------------------------------------------------------ |
| `file` | string | - | 收到的语音文件名CQ 码的 `file` 参数),如 `0B38145AA44505000B38145AA4450500.silk` |
| `out_format` | string | - | 要转换到的格式,目前支持 `mp3`、`amr`、`wma`、`m4a`、`spx`、`ogg`、`wav`、`flac` |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------ | -------- | ------------------------------------------------------------ |
| `file` | string | 转换后的语音文件路径,如 `/home/somebody/cqhttp/data/record/0B38145AA44505000B38145AA4450500.mp3` |
### getImage()
获取图片。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------ | -------- | ------ | ------------------------------------------------------------ |
| `file` | string | - | 收到的图片文件名CQ 码的 `file` 参数),如 `6B4DE3DFD1BD271E3297859D41C530F5.jpg` |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------ | -------- | ------------------------------------------------------------ |
| `file` | string | 下载后的图片文件路径,如 `/home/somebody/cqhttp/data/image/6B4DE3DFD1BD271E3297859D41C530F5.jpg` |
### canSendImage()
检查是否可以发送图片。
参数:无
响应数据
| 字段名 | 数据类型 | 说明 |
| ------ | -------- | ------ |
| `yes` | boolean | 是或否 |
### canSendRecord()
检查是否可以发送语音,返回同上。
### getStatus()
获取插件运行状态。
参数:无
响应数据
| 字段名 | 数据类型 | 说明 |
| -------- | -------- | -------------------------------------------------------- |
| `online` | boolean | 当前 QQ 在线,`null` 表示无法查询到在线状态 |
| `good` | boolean | 状态符合预期,意味着各模块正常运行、功能正常,且 QQ 在线 |
| ...... | - | OneBot 实例自行添加的其他内容 |
通常情况下建议只使用 `online` 和 `good` 这两个字段来判断运行状态,因为根据 OneBot 实现的不同,其它字段可能完全不同。
### getVersionInfo()
获取版本信息
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------------ | -------- | ----------------------------- |
| `app_name` | string | 应用标识,如 `mirai-native` |
| `app_version` | string | 应用版本,如 `1.2.3` |
| `protocol_version` | string | OneBot 标准版本,如 `v11` |
| …… | - | OneBot 实现自行添加的其它内容 |
### setRestartPlugin()
重启 OneBot 客户端
由于重启 OneBot 实现同时需要重启 API 服务,这意味着当前的 API 请求会被中断,因此需要异步地重启,接口返回的 `status` 是 `async`。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------- | -------- | ------ | ------------------------------------------------------------ |
| `delay` | number | `0` | 要延迟的毫秒数,如果默认情况下无法重启,可以尝试设置延迟为 2000 左右 |
响应数据:无
### cleanCache()
清理 OneBot 客户端的缓存。
参数:无
响应数据:无
### callExtendedAPI() (扩充 API
用来调用 OneBot 标准之外扩展出来的自定义 API。
使用不同 OneBot 客户端时,可能有一些 API 不在上方的 OneBot 标准里,这时可以使用此方法进行额外调用。
参数
| 字段名 | 数据类型 | 默认值 |
| -------- | -------- | ------ |
| `action` | string | 必填 |
| `params` | array | `[]` |
例子
```php
$result = $bot->callExtendedAPI("get_group_root_files", ["group_id" => 123456]);
//这里以 go-cqhttp 扩展的一个获取群文件的 API 为例
var_dump($result["data"]);
// 输出群文件列表
```

View File

@@ -0,0 +1,43 @@
# 单例类SingletonTrait
单例类,顾名思义,就是让用户声明的类拥有单例的特性,而这一组件引入的方式也最直接。它是一个 PHP 的 `trait`
我们传统写单例类的方式很手动,比如下面这样:
```php
<?php
class Foo {
public $test = 0;
private static $instance;
public static function getInstance() {
if (null === self::$instance) {
self::$instance = new Foo();
}
return self::$instance;
}
}
Foo::getInstance()->test = 4;
$obj = Foo::getInstance()->test;
var_dump($obj); // 4
```
这就要求我们每个需要声明为单例的类都写一个成员静态方法和成员静态变量。
框架使用了 PHP Trait 来快速让一个类支持这一特性:
```php
<?php
use ZM\Utils\SingletonTrait;
class Foo {
use SingletonTrait;
public $test = 0;
}
Foo::getInstance()->test = 5;
var_dump(Foo::getInstance()->test);
```
只需要在类中使用:`use \ZM\Utils\SingletonTrait;` 一句话即可。

View File

@@ -0,0 +1,73 @@
# SpinLock 自旋锁
前面讲到 LightCache 轻量缓存在特定的情况下为了保证数据不被多进程的因素导致丢失或覆盖,在高并发情况下修改数据需要加锁,所以炸毛框架内置了 SpinLock 自旋锁。
## 配置
自旋锁使用无需配置,和 LightCache 同源。
## 使用
定义:`ZM\Store\Lock\SpinLock`
### SpinLock::lock($key)
给信号量 `$key` 上锁。如果该信号量已经被上锁,则原地等待直到其他资源释放锁。
```php
SpinLock::lock("foo");
```
### SpinLock::unlock($key)
给信号量 `$key` 释放锁。
```php
SpinLock::unlock("foo");
```
### SpinLock::tryLock($key)
给信号量 `$key` 上锁。如果该信号量已经被上锁,则立刻返回 false。
```php
SpinLock::lock("foo");
```
## 综合实例
我们这里以之前在 LightCache 中的实例进行继续讲解,如何给之前那样的情况加锁:
```php
/**
* @RequestMapping("/test")
*/
public function test() {
SpinLock::lock("web_count"); // 加上这行
$s = LightCache::get("web_count");
if($s === null) $s = 1;
else $s += 1;
LightCache::set("web_count", $s);
SpinLock::unlock("web_count"); // 再加上这行
return "<h1>It works!</h1>";
}
```
加两行就 OK。这时再使用压测工具请求 200000 次,值就会是 200000 了!
原理剖析:在 LightCache 获取前,先对此内容上锁,这时如果其他进程有同时也在执行这个代码的时候,就会在 `SpinLock::lock()` 这行代码处原地等待,防止继续执行。等前面的那个进程执行到 `SpinLock::unlock()` 释放锁时,其他进程才可继续执行,从而避免了多个进程并行执行这段代码导致的数据错乱。
!!! error "警告"
使用锁时务必谨慎,如果不按照下面的规则使用自旋锁可能导致 CPU 占用率上升。
自旋锁使用约定:
- 使用 `SpinLock::lock()` 指定信号量名称时必须指定为字符串,且最好与你的 LightCache 缓存名称相同。
- 使用 `lock()` 时最好紧跟在 `LightCache::get()` 代码前。
- 使用自旋锁后,`LightCache::get()``LightCache::set()` 之间的代码段一定不能有 **读写文件、数据库操作和网络请求** 等代码,最好为纯 PHP 逻辑代码,且越短越好,如示例代码。
-`LightCache::set()` 后最好紧跟 `SpinLock::unlock()`
## 性能
使用自旋锁几乎没有性能损失,自旋锁要比其他类型的锁性能强很多,在上方举例使用的 `ab` 压测工具测试 100万请求 下使用自旋锁和不适用自旋锁的测试成绩时间分别为7.4s 和 6.9s。

272
docs/component/zmrequest.md Normal file
View File

@@ -0,0 +1,272 @@
# ZMRequestHTTP 客户端)
框架提供了轻量的 HTTP 请求发起工具类,直接静态调用即可。
命名空间:`use ZM\Requests\ZMRequest;`
!!! warning "注意"
在使用 Swoole 4.6.0 以下(不包含)的版本时,最好使用 Swoole 官方推荐的 Saber 或者 ZMRequest 这个轻量的 HTTP 请求客户端,不要使用 curl_exec因为在老版本的 Swoole 上对 curl 的协程 Hook 支持不是很完善。
## ZMRequest::get()
发起 GET 请求。
定义:`ZMRequest::get($url, $headers = [], $set = [], $return_body = true)`
全局函数别名:`zm_request_get($url, $headers = [], $set = [], $return_body = true)`
`$url`:要请求的 url`http://captive.apple.com/`
`$headers`:要请求的 Headers例如`["User-Agent" => "Chrome"]`,数组形式
`$set`:请求时的一些设置,例如超时时间等等。详见下方“设置参数”
`$return_body`:是否只返回请求回来的内容部分,默认为 true如果为 false 时则会返回一个 `\Swoole\Coroutine\Http\Client` 对象,可查阅 [Swoole 文档](http://wiki.swoole.com/#/coroutine_client/http_client) 进行接下来的一系列操作。
如果 `$return_body` 为 true但是请求失败HTTP 状态码不是 200 或无法连接到目标服务器或者无法解析域名等问题)时,方法会返回 false否则会返回内容。
返回值:`false|string|\Swoole\Coroutine\Http\Client`
```php
$r = ZMRequest::get("http://captive.apple.com/", ["User-Agent" => "Chrome"]);
echo $r.PHP_EOL; // <HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>
```
```php
$r = zm_request_get("http://captive.apple.com/", [], [], false);
echo $r->body.PHP_EOL; // 这行输出和上方的一致
dump($r);
/*
^ Swoole\Coroutine\Http\Client {#170
+errCode: 0
+errMsg: ""
+connected: false
+host: "captive.apple.com"
+port: 80
+ssl: false
+setting: array:1 [
"timeout" => 15.0
]
+requestMethod: "GET"
+requestHeaders: []
+requestBody: null
+uploadFiles: null
+downloadFile: null
+downloadOffset: 0
+statusCode: 200
+headers: array:4 [
"content-type" => "text/html"
"content-length" => "68"
"date" => "Thu, 07 Jan 2021 06:22:32 GMT"
"connection" => "keep-alive"
]
+set_cookie_headers: null
+cookies: null
+body: "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>"
}
*/
```
## ZMRequest::post()
发送一个 POST 请求。
定义:`ZMRequest::post($url, array $header, $data, $set = [], $return_body = true)`
全局函数别名:`zm_request_post($url, array $header, $data, $set = [], $return_body = true)`
`$url`:同上,填入 url必填
`$header`:请求的 Headers必填数组形式例如 `["Content-Type" => "application/json"]`
`$data`:请求的数据体,默认应该传入数组,如果传入 `array` 类型,则会默认当作 `Content-Type: application/x-www-form-urlencoded` 方式自动转码和转换,例如 `["key1" => "b1", "key2" => "b2"]` 会变成 `key1=b1&key2=b2`
`$set`:同上,见下面的设置参数部分。
`$return_body`:同上。
```php
$s = ZMRequest::post("http://captive.apple.com/", ["Content-Type" => "application/json"], json_encode(["key1" => "value1"]));
```
## ZMRequest::request()
发起自定义一切参数的 HTTP 请求。
参数:
- `$url`请求的链接自动解析端口、HTTPS、DNS 等操作
- `$attribute`:请求的属性,示例见下方
- `$return_body`:可选参数,`bool` 类型,和上面的 `$return_body` 参数意义相同
其中 `$attribute` 参数对应可设置的有:
- `method`:可选 `GET``POST` 等 HTTP 请求的方式
- `set`:设置 Swoole 客户端的参数
- `headers`:要请求的 HTTP Headers
- `data`:请求的 body 数据,为数组时自动转换头部为 `x-www-form-urlencoded`
- `file`:要发送的文件,数组,可选多个文件
例1使用 GET 请求发送带有 Body 的 HTTP 请求
```php
$r = ZMRequest::request("http://example.com", [
"method" => "GET",
"data" => [
"key1" => "value1"
]
]);
```
例2设置请求超时时间并指定自定义头部
```php
$r = ZMRequest::request("http://example.com", [
"method" => "POST",
"headers" => [
"X-Custom-Header" => "Hello world",
"User-Agent" => "HEICORE"
],
"set" => ["timeout" => 10.0]
]);
```
例3发送文件和 data
```php
$r = ZMRequest::request("http://example.com/sendfile", [
"file" => [
[
"path" => "/path/to/image1.jpg", // path字段必填
"name" => "file1", // name字段必填这个是 POST 过去的 key
//"mime_type" => "image/jpeg", // 可选字段,底层会自动推断
//"filename" => "a.jpg", // 可选字段,文件名称
//"offset" => 0, // 可选字段,可以从指定文件的中间部分开始传输数据,此特性用于断点续传
//"length" => 1024 // 可选字段,默认为整个文件的尺寸
],
[
"path" => "/path/to/image2.jpg",
"name" => "file2"
]
],
"data" => [
"key1" => "value1"
]
]);
```
## ZMRequest::downloadFile()
下载文件到本地。
定义:`ZMRequest::downloadFile($url, $dst = null)`
`$url`:不多讲,下载链接。
`$dst`:本地位置,例如 `/tmp/hello.html`
下载成功返回 true 或指定的文件位置,失败返回 false。
```php
ZMRequest::downloadFile("http://captive.apple.com/", "/tmp/apple.html");
```
## ZMRequest::websocket()
创建一个 WebSocket 连接。因为 Swoole 提供的是同步协程的方案,但对于 WebSocket 这样的全双工通信,反而不是一个好的代码逻辑,炸毛框架将此同步协程的方案封装成了异步事件调用的方式。
定义:`ZMRequest::websocket($url, $set = ['websocket_mask' => true], $header = [])`
返回:一个 `\ZM\Requests\ZMWebSocket` 对象
效果等同于:`$s = new \ZM\Requests\ZMWebSocket($url, $set = ['websocket_mask' => true], $header = [])`
这个是 ZMRequest 扩展而来的异步 WebSocket 客户端,可供方便地连接、收发 WebSocket 消息所定制。
命名空间:`\ZM\Requests\ZMWebSocket`
```php
$ws = ZMRequest::websocket("ws://127.0.0.1:12345/"); //使用工具函数
// $ws = new ZMWebSocket("ws://127.0.0.1:12345/"); //直接构造
if($ws->is_available) {
$ws->onMessage(function(\Swoole\WebSocket\Frame $frame, $client) {
var_dump($frame->data);
});
$ws->onClose(function($client){
Console::info("Websocket closed.");
});
$result = $ws->upgrade();
var_dump($result);
}
```
### 属性
#### is_available
`bool` 类型,用于判断构造对象是否成功或链接是否可用。在构建新的对象并执行 `upgrade()` 前,如果 ws 链接没有问题,则会变为 true`onClose()` 回调执行后,此值变回 false。
### 方法
#### __construct()
客户端对象的构造方法。
参数:
- `$url`:要请求到的 WebSocket 目标地址,必须以 `ws(s)://` 开头
- `$set`可选Swoole 客户端的参数,例如超时、是否使用 `websocket_mask` 等,如果为空数组则默认为 `["websocket_mask" => true]`,具体可设置的内容见 [Swoole 文档](https://wiki.swoole.com/#/coroutine_client/http_client?id=set)
- `$header`:可选,请求的头部信息,数组
```php
$a = new ZMWebSocket("ws://127.0.0.1:8080/", ["websocket_mask" => true], [
"User-Agent" => "Firefox"
]);
```
#### onMessage()
设置收到消息的回调函数。
回调的参数:
- `$frame``Swoole\WebSocket\Frame` 类型,消息帧,一般只用 `$frame->data` 获取数据,具体见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)
- `$client``Swoole\Coroutine\Http\Client` 类型,为客户端本身的对象,用于 push 数据等
```php
$a->onMessage(function($frame, $client){
echo "收到消息:".$frame->data.PHP_EOL;
$client->push("hello world");
});
```
#### onClose()
设置连接断开后执行的回调函数。
回调的参数:
- `$client`:同上,但断开连接后不能使用 `push()` 发送数据了,只建议作为重连等机制的使用
```php
$a->onClose(function($client){
echo "WS 链接断开了!".PHP_EOL;
});
```
#### upgrade()
发起连接。
返回值:`true|false`,当为 `true` 时代表握手成功,此时可以在回调里愉快地收发消息了。如果为 `false` 表明握手失败。
!!! warning "注意"
这里由于是协程转异步,所以不能确定 `upgrade()``onMessage()` 哪个先会被触发(一般情况下如果服务器不是立刻响应回包信息,总是会先返回 `upgrade()` 的结果。
## 设置参数
见:[Swoole - HTTP 客户端](http://wiki.swoole.com/#/coroutine_client/http_client?id=set)

23
docs/component/zmutil.md Normal file
View File

@@ -0,0 +1,23 @@
# ZMUtil 杂项工具类
调用前先 use`use ZM\Utils\ZMUtil;`
## ZMUtil::stop()
停止框架运行。
## ZMUtil::reload()
重载框架,这会断开所有到框架的连接和重载所有 `src/` 目录下的用户源码并重新加载所有 Worker 进程。
## ZMUtil::getModInstance()
根据类名称拿到此类的单例(前提是目标的类的构造函数为空)。
```php
class ASD{
public $test = 0;
}
ZMUtil::getModInstance(ASD::class)->test = 5;
```

View File

@@ -0,0 +1,3 @@
# 自定义注解
TODO师傅莫催快肝完了

View File

@@ -0,0 +1,119 @@
# 事件分发器(进阶)
事件分发器是以上所有注解事件执行函数的一个分发器,如果你在上一章已经学会了如何创建自定义注解,那么本章就来说明如何用内置的事件分发器进行分发自定义事件。
如果你不需要了解或自定义有关事件分发的功能,此处可无需阅读。
## 属性
- 类名:`ZM\Event\EventDispatcher`
## 方法
### EventDispatcher::interrupt()
阻断当前正在运行的事件,只能在事件内部被调用的函数中实现。
### __construct()
构造方法。
```php
EventDispatcher::__construct(string $class = '')
```
初始化一个事件分发器,可进行一系列设置,对事件分发做限定。
#### 参数
`$class`:设置要分发的事件对应的注解类名,支持自定义注解(例如 `CQMessage::class`
### setRuleFunction()
设置函数触发规则判定的函数(就是在执行事件函数前执行的规则判定)
```php
setRuleFunction(callable $rule = null)
```
#### 参数
`$rule`:支持回调或闭包。闭包的参数为执行对应事件函数所绑定的注解事件对象。
```php
$dispatcher = new EventDispatcher(CustomEvent::class);
$dispatcher->setRuleFunction(function($obj) {
return $obj->name == "zhamao" ? true : false;
});
```
上方的 `$obj` 就是 CustomEvent 类的实例,参数绑定为注解中对应的参数。
### setResultFunction()
设置事件函数返回值处理的回调函数。
```php
setReturnFunction(callable $return_func)
```
#### 参数
`$return_func`:设置事件函数返回值处理的回调函数,回调参数绑定为对应单独事件函数的返回值。
```php
$dispatcher = new EventDispatcher(CustomEvent::class);
$dispatcher->setReturnFunction(function($return) {
if (is_string($return)) Console::info("函数返回了 ".$return);
});
```
### dispatchEvents()
开始分发事件。
```php
dispatchEvents(...$params)
```
#### 参数
自定义参数,这里填入的参数将被填入被分发的函数参数中。
```php
$dispatcher->dispatchEvents("foo", "bar");
```
```php
<?php
class Test {
/**
* @CustomEvent("zhamao")
*/
public function test($arg1, $arg2) {
echo "$arg1: $arg2"; //将输出 "foo: bar"
}
}
```
## 机制
事件分发器的机制说简单不简单,说复杂也不复杂,它和中间件有着非常大的关系,因为它会自动检测和识别所要执行的函数有没有中间件,并且根据顺序进行执行。
在炸毛框架内部,一个完整的事件流程和中间件的关系如下图:
![Untitled Diagram](../assets/img/diagram3.dbb4e32e.png)
对于同一事件的优先级和响应顺序,优先级的关系如下图:
![Untitled Diagram](../assets/img/Untitled Diagram.png)
对于事件内单个事件被调用的单个函数下如果存在多个中间件,中间件模型和事件的关系如下图:
![Untitled Diagram-2](../assets/img/diagram4.16ce39ca.png)
## 实战例子
我们假设 CustomEvent 是我们的自定义注解。还没写完,这部分太复杂了,而且举例子也不好举例,这块应该也不用着急更新。
TODO待完成

View File

@@ -0,0 +1,387 @@
# 框架核心注解事件
框架核心注解事件区别于机器人和路由注解事件,这里框架注解事件都是**直接**或封装调用 Swoole 的回调事件的,所以对一些比较底层或者基础的操作都在这里做,例如收到 HTTP 或 WebSocket 连接后执行的事件函数。
## OnOpenEvent()
当有 WebSocket 连接接入框架时,触发注解事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------- |
| 名称 | `@OnOpenEvent` |
| 触发前提 | 当有 WebSocket 连接接入框架时,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnOpenEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | -------- | ------------------------------------------------------------ | ---- |
| connect_type | `string` | 限定连接的类型,通过炸毛框架支持的方式指定传入类型,详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client) | |
### 用法
```java
@OnOpenEvent("foo")
@OnOpenEvent(connect_type="default")
```
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
## OnCloseEvent()
当有 WebSocket 连接断开框架时,触发注解事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------- |
| 名称 | `@OnCloseEvent` |
| 触发前提 | 当有 WebSocket 连接断开框架时,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnCloseEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | -------- | ------------------------------------------------------------ | ---- |
| connect_type | `string` | 限定连接的类型,通过炸毛框架支持的方式指定传入类型,详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client) | |
### 用法
```java
@OnCloseEvent("foo")
@OnCloseEvent(connect_type="default")
```
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
## OnRequestEvent()
当 HTTP 请求接入时,触发注解事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------- |
| 名称 | `@OnRequestEvent` |
| 触发前提 | 当 HTTP 请求接入时,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnRequestEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | --------------------------------------------- | ------------------------ | ---------------- |
| rule | `string`,必须是可执行且返回 bool 的 PHP 代码 | 前置条件 | 空rule 为 true |
| level | `int` | 事件优先级(越大越靠前) | 20 |
## OnMessageEvent()
当有 WebSocket 连接接入框架后发送过来消息,触发注解事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------------------- |
| 名称 | `@OnMessageEvent` |
| 触发前提 | 当有 WebSocket 连接接入框架后发送过来消息,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnMessageEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | -------- | ------------------------------------------------------------ | ---- |
| connect_type | `string` | 限定连接的类型,通过炸毛框架支持的方式指定传入类型,详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client) | |
### 用法
```java
@OnMessageEvent("foo")
@OnMessageEvent(connect_type="default")
```
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
## OnPipeMessageEvent()
当有 其他 Worker 进程通信发来指令激活响应。2.2.0 版本可用)
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------------------- |
| 名称 | `@OnPipeMessageEvent` |
| 触发前提 | 当有 WebSocket 连接接入框架后发送过来消息,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnPipeMessageEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | -------- | ------------ | ---- |
| action | `string` | 限定动作名称 | |
### 用法
```java
@OnPipeMessageEvent("foo")
@OnPipeMessageEvent(action="bar")
```
### 事件绑定参数
`$data`: 数组,内容如下:
```php
[
"action" => "你的上面的名称",
... //其他自己发送时随便定义,带什么都行
]
```
## OnSwooleEvent()
绑定 Swoole 所相关的事件,例如 WebSocket 接入、收到 WS 消息、关闭 WS 连接HTTP 请求到达等。这个是旧的统一的 Swoole 事件分发注解。**请尽量使用上面几个新的注解**。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------ |
| 名称 | `@OnSwooleEvent` |
| 触发前提 | 当参数指定的 `type` 对应的事件被触发后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnSwooleEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | -------------------------------------------------------- | ----------------------------------------------- | ---------------- |
| type | `string`,支持填入 `open``request``close``message` | 限定事件的类型,**必填** | |
| rule | `string`,必须是可执行且返回 bool 的 PHP 代码 | 例如判断连接是否为 QQ 机器人(`connectIsQQ()` | 空rule 为 true |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
## OnStart()
在框架加载后执行的注解事件,用于初始化 Worker 进程,此注解事件会在 Worker 进程中执行,且可以指定在哪个 Worker 进程中执行。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------ |
| 名称 | `@OnStart` |
| 触发前提 | 在框架加载后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnStart` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| --------- | ------------------------------------------------------------ | ------------------------ | ---- |
| worker_id | `int`,要在哪个 Worker 进程上执行,默认为 0范围是 0{你设定的 Worker 数量-1},如果是 -1 的话,则会在所有 Worker 进程上触发。 | 限定只执行的 Worker 进程 | |
## OnTick()
在框架加载后创建毫秒计时器。
### 属性
| 类型 | 值 |
| ---------- | ----------------------------- |
| 名称 | `@OnTick` |
| 触发前提 | 在框架加载后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnTick` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| --------- | ------------------------------------------------------------ | ------------------------ | ---- |
| tick_ms | `int`**必填**,间隔的毫秒数,例如 1 秒间隔为 `1000`,范围大于 0小于 86400000。 | | |
| worker_id | `int`,要在哪个 Worker 进程上执行,默认为 0范围是 0{你设定的 Worker 数量-1},如果是 -1 的话,则会在所有 Worker 进程上触发。 | 限定只执行的 Worker 进程 | |
## OnSetup()
在框架加载前执行的代码。此部分代码是在主进程执行的,不可在此事件中使用任何协程相关的功能。
比如我们要改变所有进程的 ini 设置,这时使用 `@OnStart(-1)` 这样只设置了 Worker 进程的内容,而主进程和管理进程无法被覆盖到。如果需要设置全局的一些配置,务必在此 `@OnSetup` 注解下执行。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------ |
| 名称 | `@OnSetup` |
| 触发前提 | 在框架加载前激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnSetup` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
无。
## 示例1机器人连接框架后输出信息
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
class Hello {
/**
* 在机器人客户端连接框架后向终端输出信息
* @OnOpenEvent("qq")
* @param $conn
*/
public function onConnect(ConnectionObject $conn) {
Console::info("机器人 " . $conn->getOption("connect_id") . " 已连接!");
}
}
```
这里的 Console 是终端输出组件,详情见组件一栏对应的文档查询。
## 示例2阻断 Chrome 访问框架时多访问一次的问题)
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Event\EventDispatcher;
class Hello {
/**
* 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰
* @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
*/
public function onRequest() {
EventDispatcher::interrupt();
}
}
```
其中 EventDispatcher 为事件分发器interrupt 是通用阻断方法,如果你平常只使用阻断,则只需掌握这一个方法即可,`EventDispatcher::interrupt()` 在所有事件内可用。
## 示例3接收 WS 客户端发来的数据)
见 [接入 WebSocket 客户端](/advanced/connect-ws-client/)。
## 示例4使用 OnStart 给所有 Worker 进程写入缓存提速)
如果你有一些数据存到了文件、数据库中,且是只读不写的,那么就可以使用此方法将这个文件或者数据库的内容读入 Worker 进程的内存中进行使用来提速。
假设我们有一个大文件 json里面存着一份题库例如
```json
{
"0": {
"question": "法的调整对象是( )。",
"answer": {
"A": "行为关系",
"B": "思想关系",
"C": "利益关系",
"D": "各种社会资源"
},
"key": "A",
"answer_type": 0
},
"1": {
"question": "法律与其他社会规范的区别在于( )。",
"answer": {
"A": "是调整人们行为的规范",
"B": "有约束力",
"C": "由国家强制力保证执行",
"D": "规定制裁措施"
},
"key": "C",
"answer_type": 0
},
.....
}
```
那么我们可以使用 OnStart 来实现一个,将此文件读取到每个 Worker 进程中,并且快速取用的功能(以下做了一个简单的查题功能):
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\CQ\CQCommand;
use ZM\Console\Console;
class Hello {
public static $tiku = [];
/**
* @OnStart(-1)
*/
public function onStart() { // 注意,此函数将会在每个 Worker 执行一次
$file = file_get_contents("tiku.json"); //从文件读取json
$json = json_decode($file, true); //json解析
Hello::$tiku = $json; //将解析后的数组以静态变量的方式存到每个 Worker 的内存中
Console::success("加载题库完成!");
}
/**
* @CQCommand("找题")
*/
public function findQuestion() {
$tiku_id = ctx()->getNumArg("请输入题目的序号");
if(!isset(Hello::$tiku[$tiku_id])) return "题目id为".$tiku_id."的题目不存在!";
$timu = Hello::$tiku[$tiku_id];
$msg = "题目名称:".$timu["question"];
foreach($timu["answer"] as $k => $v) {
$msg .= "\n".$k.". ".$v;
}
$msg .= "\n正确答案:".$timu["key"];
return $msg;
}
}
```
终端效果:(我们假设运行框架的电脑是四核 CPU
```log
[14:28:00] [S] [#0] 加载题库完成!
[14:28:00] [S] [#2] 加载题库完成!
[14:28:00] [S] [#1] 加载题库完成!
[14:28:00] [S] [#3] 加载题库完成!
```
聊天效果:
<chat-box>
) 找题 1
( 题目名称:法律与其他社会规范的区别在于( )。\nA. 是调整人们行为的规范\nB. 有约束力\nC. 由国家强制力保证执行\nD. 规定制裁措施\n正确答案C
</chat-box>
## 示例5创建每分钟自动执行的爬虫
```php
/**
* @OnTick(tick_ms=60000,worker_id=0)
*/
public function onCrawl() {
$data = Foo::bar(); //这里是你自己写的要爬的接口等等一系列操作
LightCache::set("your_data_key_name", $data); //将爬虫数据存入 LightCache 轻量缓存
}
```

View File

@@ -53,4 +53,18 @@ class Hello {
机器人开发过程中常见的 `@CQCommand`,或者是 HTTP 服务器路由绑定 `@RequestMapping` 都是相当于由对应注解代表了事件,而 `@Middleware``@Closed` 等这类注解显然不代表任何事件,只能当作这个函数或类的修饰属性而已。代表了事件的注解,我们称之为**注解事件**,它会在某种事件达成条件后触发注解下方的函数本身。
值得注意的是,注解事件本身概念是我凭空捏造的,我不好解释所以只能创造这么一个词来代指这一抽象的概念,硬要解释的话,大致就好比一个社区里有一个卖牛奶的,有几家人订阅了每日上门送牛奶的服务,只要你打了“给我配送牛奶”的注解,他就会上门。而它送的不止一种奶,可以给你个性化定制,比如让卖牛奶的给你带包糖带瓶水,而描述这个的注解就只能做一个之前注解的修饰。假设你只写了带包糖的注解,没有写给我配送牛奶的注解,那他永远也不会给你送牛奶和糖过来。
值得注意的是,注解事件本身概念是我凭空捏造的,我不好解释所以只能创造这么一个词来代指这一抽象的概念,硬要解释的话,大致就好比一个社区里有一个卖牛奶的,有几家人订阅了每日上门送牛奶的服务,只要你打了“给我配送牛奶”的注解,他就会上门。而它送的不止一种奶,可以给你个性化定制,比如让卖牛奶的给你带包糖带瓶水,而描述这个的注解就只能做一个之前注解的修饰。假设你只写了带包糖的注解,没有写给我配送牛奶的注解,那他永远也不会给你送牛奶和糖过来。
## 阻断事件
由于炸毛框架内的注解事件统一由一个通用的事件分发器进行分发,所以你在任何注解事件内都可以用通用的方式阻断当前正在运行的事件。
首先就是要记得先 use 事件分发器的类:`use ZM\Event\EventDispatcher;`
```php
EventDispatcher::interrupt();
EventDispatcher::interrupt($data); // 也可以带返回值,自定义注解事件时有用。
```

167
docs/event/middleware.md Normal file
View File

@@ -0,0 +1,167 @@
# 中间件注解
对于 `@RequestMapping` 等注解绑定的事件函数,还支持中间件,可以完成 Session 会话、认证、日志记录等功能。中间件是用于控制 `请求到达``响应请求` 的整个流程的。从一定意义上来说相当于切面编程AOP
在炸毛框架中,中间件最直白的意思就是注解事件执行前、执行后、执行过程中可进行插入代码但不破坏原有代码。
```伪代码
@中间件1
@带条件的注解1
function 我的方法() {
blablabla...
}
//插入中间件,下面是执行流程
-> 判断注解1的执行条件是否为true
-> 中间件1的前置插入代码
-> 我的方法
-> 中间件1的后置插入代码
X -> 我的方法有异常时执行中间件1的异常处理
//不插入中间件,下面是执行流程
-> 判断注解1的执行条件是否为true
-> 我的方法
X -> 有异常则直接跳到最外层被框架捕获
```
中间件和事件分发器是紧密相连的,炸毛框架的内部分发器在分发注解事件的过程中会判断将要执行的事件是否含有中间件,框架内部执行流程图见下一章:事件分发器。
## 定义中间件
下方就是一个可以在终端打印路由函数运行的总时间的中间件,只需给中间件标明里面的 `@MiddlewareClass` 到中间件的类上就可以了。
```php
<?php
namespace Module\Middleware;
use Exception;
use ZM\Annotation\Http\HandleAfter;
use ZM\Annotation\Http\HandleBefore;
use ZM\Annotation\Http\HandleException;
use ZM\Annotation\Http\MiddlewareClass;
use ZM\Console\Console;
use ZM\Http\MiddlewareInterface;
/**
* @MiddlewareClass("timer")
*/
class TimerMiddleware implements MiddlewareInterface
{
private $starttime;
/**
* @HandleBefore()
* @return bool
*/
public function onBefore() {
$this->starttime = microtime(true);
return true;
}
/**
* @HandleAfter()
*/
public function onAfter() {
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms.");
}
/**
* @HandleException(\Exception::class)
* @param Exception $e
* @throws Exception
*/
public function onException(Exception $e) {
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred.");
throw $e;
}
}
```
技术要素:
1. 将需要声明为中间件的 class 类标上注解 `@MiddlewareClass`,并带有参数,参数为中间件名称,字符串即可。
2. 使用 `@MiddlewareClass` 的需要先 use`use ZM\Annotation\Http\MiddlewareClass;`
3. 类成员中声明执行前插入、执行后插入和异常捕获函数也需要注解,分别是 `@HandleBefore``@HandleAfter``@HandleException`,都在 `ZM\Annotation\Http` 命名空间下。
4. `@HandleBefore` 类似 `@CQBefore`,需要返回 bool 类型值,如果不返回,默认为 true。当为 true 时,则不会阻断执行事件函数本身。
5. 中间件内的函数不可被绑定为注解事件。
6. `@HandleException` 可以写多个,但其中的参数只能写想要捕获的异常的类全称,例如 `\Exception::class` 返回的就是 `\\Exception``\ZM\Exception\InterruptException::class` 返回的是 `ZM\\Exception\\InterruptException`,举的这两个例子这样写都是可以的。
7. 如果 `@HandleException` 有多个的话,则会按照声明顺序依次让其捕获,看其是否为要被捕获的错误的类或父类。例如在最后一个 `@HandleException` 捕获 `\Throwable` 则最终此中间件会捕获所有异常。
8. 中间件内可以正常使用和注解事件执行的内容同一上下文,例如 `@RequestMapping` 下你可以使用 `ctx()->getRequest()``@CQMessage` 可以使用 `ctx()->getMessage()` 等,以此类推。
## 使用中间件
如上图,我们举了一个非常简单的例子,打印出函数执行的时间。我们假设一个需要耗时较长的函数:
```php
/**
* @RequestMapping("/testTime")
* @Middleware("timer")
*/
public function testTime() {
zm_sleep(3); //等待3秒再返回
return "OK!";
}
```
在执行后,你的执行结果可能为:
```
[11:18:56] [I] [#0] Using 3000.07 ms
```
或者,我们也可以将中间件注解写到类上:
```php
/**
* @Middleware("timer")
*/
class Hello {
/**
* @RequestMapping("/test/ping")
*/
public function ping(){
return "pong";
}
/**
* @RequestMapping("/test/ping2")
*/
public function ping2(){
return "pong2";
}
}
```
效果等同于给此类下每个注解事件写一个 `@Middleware`
## 使用多个中间件
多个使用中间件可以同时生效多个流程的中间件。这里要注意,多个中间件中,`@HandleBefore` 方法中如果返回了 `false`,则不会执行接下来的中间件和事件本身要触发的函数,直接跳到最后此中间件的 `@HandleAfter` 方法。
```php
/**
* @CQCommand("你好")
* @Middleware("timer1")
* @Middleware("timer2")
*/
public function hello() { return "成功执行!"; }
```
## 使用中间件捕获异常
通常情况下,如果用户定义的函数内抛出了异常(包括 `message` 等事件),会返回到框架基层去返回默认定义的内容。如果想自己捕获可以使用 `try/catch` ,但不方便复用,多处使用的话就需要重复写代码。这里可以使用中间件的异常处理方便地捕获错误。这个函数写到中间件类里即可
```php
/**
* @HandleException(\Exception::class)
* @param Exception|null $e
*/
public function onThrowing(?Exception $e) {
ctx()->getResponse()->endWithStatus(500, "Error on this.");
}
```
这里的 `@HandleException` 中的参数为要捕获的类名,注意这里面的类名的命名空间需要写全称,不能上面 use 再使用,否则会无法找到异常类。
`context()` 为获取当前协程空间绑定的 `request``response` 对象。

View File

@@ -0,0 +1,328 @@
# 机器人注解事件
QQ 机器人事件是指 CQHTTP 插件发来的 Event 事件,被框架处理后触发到单个类中方法的事件。
为了便于开发,这里的注解类对应 CQHTTP 插件返回的 `post_type` 类型,对号入座即可。
!!! tip "提示"
在使用注解绑定事件过程中,如果无 **必需** 参数,可一个参数也不写,效果就是此事件任何情况下都会调用此方法。例如:`@CQMessage()`
事件是用户需要从 OneBot 被动接收的数据,有以下几个大类:
- [消息事件](#cqmessage),包括私聊消息、群消息等,被 [`@CQCommand`](#cqcommand)`@CQMessage` 注解处理。
- [通知事件](#cqnotice),包括群成员变动、好友变动等,被 `@CQNotice` 注解事件处理。
- [请求事件](#cqrequest),包括加群请求、加好友请求等,被 `@CQRequest` 注解事件处理。
- [元事件](#cqmetaevent),包括 OneBot 生命周期、心跳等,被 `@CQMetaEvent` 注解事件处理。
## CQMessage()
QQ 收到消息后触发的事件对应注解。
### 属性
| 类型 | 值 |
| ---- | ----------- |
| 名称 | `@CQMessage` |
| 触发前提 | 当 `post_type``message` 时触发 |
| 命名空间 | `ZM\Annotation\CQ\CQMessage` |
| 适用位置 | 方法 |
| 返回值处理 | 当方法返回字符串时,效果等同于执行 `ctx()->reply("xxx")` |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | ------------------------------------- | -------------------------------------- | ---- |
| message_type | `string`,支持填入 `private``group` | 限定消息事件的来源类型,如私聊或群消息 | 空 |
| user_id | `int64``string` | 限定消息发送用户 IDQQ 号) | 空 |
| group_id | `int64``string` | 限定消息发送来源群 IDQQ 群号) | 空 |
| message | `string` | 限定消息内容文本 | 空 |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
下面这个例子的注释用途就是:
- 在用户 QQ 为 `123456` 的用户私聊给机器人发消息后机器人回复内容。
- 用户发送文字为 `hello` 时返回 `你好啊xxx` 的消息。
=== "代码"
```php
<?php
namespace Module\Example;
use ZM\Annotation\CQ\CQMessage;
class Hello {
/**
* @CQMessage(message_type="private",user_id=123456)
*/
public function test() {
return "你和机器人私聊发送了这些文本:".ctx()->getMessage();
}
/**
* @CQMessage(message="hello")
*/
public function hello() {
return "你好啊,".ctx()->getUserId();
}
}
```
=== "效果"
<chat-box>
) 假设我是私聊机器人
( 你和机器人私聊发送了这些文本:假设我是私聊机器人
^ 假设我现在切到群里在群里发hello
) hello
( 你好啊123456
</chat-box>
## CQCommand()
此注解是对 `@CQMessage` 类别的再封装,是命令解析格式处理消息的利器。例如,你想写一个疫情上报,指令是 `疫情 城市名称`,那么此方式来解析用户消息会更加方便。
### 属性
| 类型 | 值 |
| ---------- | -------------------------------------------------------- |
| 名称 | `@CQCommand` |
| 触发前提 | 当根据参数规则匹配到用户命令式消息时触发 |
| 命名空间 | `ZM\Annotation\CQ\CQCommand` |
| 适用位置 | 方法 |
| 返回值处理 | 当方法返回字符串时,效果等同于执行 `ctx()->reply("xxx")` |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | ------------------------------------- | ------------------------------------------------------ | ---- |
| match | `string` | 匹配第一个词的命令式消息,如 `天气 北京` 中的 `天气` | 空 |
| pattern | `string` | 根据 * 号通配符进行模式匹配用户消息,如 `查询*天气` | 空 |
| regex | `string`,限定正则表达式 | 匹配正则表达式匹配到的用户消息 | 空 |
| start_with | `string` | 匹配消息开头相匹配的消息,如 `我叫炸毛`,这里写 `我叫` | 空 |
| end_with | `string` | 匹配消息结尾相匹配的消息,以 `start_with` 类推 | 空 |
| keyword | `string` | 匹配消息中有相关关键词的消息 | 空 |
| alias | `array[string]` | `match` 匹配到命令的别名,数组形式 | `{}` |
| message_type | `string`,支持填入 `private``group` | 限定消息事件的来源类型,同 `@CQMessage` | 空 |
| user_id | `int64` 或 `string` | 限定消息发送用户 ID同 `@CQMessage` | 空 |
| group_id | `int64` 或 `string` | 限定消息发送来源群 ID同 `@CQMessage` | 空 |
| level | `int` | 事件优先级(越大越靠前) | 20 |
!!! warning "注意"
在 `@CQCommand` 注解事件中,从 `match` 到 `keyword` 六个参数中,必须且只能定义一个,`alias` 目前只能和 `match` 参数同时使用;
框架内部对于同一条消息事件,优先处理 `@CQCommand` 注解事件,如果未匹配到任何注解事件,则才会继续执行 `@CQMessage` 注解事件。
- 参数 `match` 匹配模式是:遇到空格、换行就会切分,比如 `点歌 xxx yyy` 会被分割为 `[点歌,xxx,yyy]`,然后抽取第一个词做为命令去匹配,剩下的为参数。
- 参数 `pattern` 匹配模式是:\* 号位置变成参数,比如 `从*到*的随机数`,我们输入 `从1到9的随机数`,成功匹配,参数列表:`[1,9]`。
- 参数 `regex` 匹配模式为 PHP 标准的 pcre 正则表达式,比如 `([01][0-9][2][0-3]):[0-5][0-9]` 用来匹配 `22:45`。
- 参数 `start_with` `end_with` 和 `keyword` 都是根据消息内容开头、结尾或者内容包含是否匹配来匹配,这里就不多说了,你懂的。
- 参数 `alias` 用的时候一般是这样:`@CQCommand(match="你好",alias={"你好啊","你是谁"})`,用以扩充同义词下命令的适配广度。
### 用法
我们以参数 `match` 写一个简单的 demo
=== "代码"
```php
<?php
namespace Module\Example;
use ZM\Annotation\CQ\CQCommand;
class Hello {
/**
* @CQCommand(match="疫情",alias={"COVID"})
*/
public function virus(){
$city = ctx()->getNextArg("请输入城市名称");
return "城市 ".$city." 的疫情状况如下:"."{这里假装是疫情接口返回的数据}";
}
/**
* 如果选择使用 match 参数的话,可以省略 `match=`
* @CQCommand("掷硬币")
*/
public function randChoice() {
return "你看到的是:" . (mt_rand(0,1) ? "正面" : "反面");
}
/**
* @CQCommand(pattern="*把*翻译成*")
*/
public function translate() {
ctx()->getNextArg(); // 为什么需要单独调用一次呢?看下面例子就知道啦
$text = ctx()->getNextArg(); // 获取第二个星号匹配的内容
$target = ctx()->getNextArg(); // 获取第三个星号匹配的内容
// 这里 FakeTranslateAPI 是假设我们对接了一个翻译的 API开发时请替换为自己的接口。
return "翻译结果:" . FakeTranslateAPI::translate($text, $target);
}
}
```
=== "效果"
<chat-box>
) 疫情 北京
( 城市 北京 的疫情状况如下blablablabla
) COVID 香港
( 城市 香港 的疫情状况如下blablablabla
) 掷硬币
( 你看到的是:正面
) 我想把我爱你翻译成英语
( 翻译结果I love you!
</chat-box>
## CQNotice()
通知事件。
### 属性
| 类型 | 值 |
| ---------- | ----------------------------------------------------- |
| 名称 | `@CQNotice` |
| 触发前提 | 当 `post_type` 为 `notice` 时触发(通知类事件上报时) |
| 命名空间 | `ZM\Annotation\CQ\CQNotice` |
| 适用位置 | 方法 |
| 返回值处理 | 无作用 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ----------- | ------------------------------------ | ------------------------------------------------------------ | ---- |
| notice_type | `string`,支持填入 onebot 标准的内容 | 限定通知事件的类型,见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
| user_id | `int64` 或 `string` | 限定通知事件用户 IDQQ 号),同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
| group_id | `int64` 或 `string` | 限定通知事件群 IDQQ 群号),同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
| operator_id | `int64` 或 `string` | 限定操作者 QQ 号,同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
TODO先放着有时间再更。
## CQRequest()
请求事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------------------ |
| 名称 | `@CQRequest` |
| 触发前提 | 当 `post_type` 为 `request` 时触发(通知类事件上报时) |
| 命名空间 | `ZM\Annotation\CQ\CQRequest` |
| 适用位置 | 方法 |
| 返回值处理 | 无作用 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | ------------------------------------ | ------------------------------------------------------------ | ---- |
| request_type | `string`,支持填入 onebot 标准的内容 | 限定请求事件的类型,见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
| user_id | `int64` 或 `string` | 限定请求事件当事人用户 IDQQ 号),见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
| sub_type | `string` | 限定请求事件来源群 IDQQ 群号),见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
| comment | `string` | 限定验证消息内容,见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
TODO先放着有时间再更。
## CQMetaEvent()
元事件元事件不属于用户交互的一部分消息、通知、请求三大类事件是与聊天软件直接相关的、机器人真实接收到的事件除了这些OneBot 自己还会产生一类事件,这里称之为「元事件」,例如生命周期事件、心跳事件等,这类事件与兼容 OneBot 的客户端和炸毛框架本身的运行状态有关,而与聊天软件无关。元事件的上报方式和普通事件完全一样。
### 属性
| 类型 | 值 |
| ---------- | ----------------------------------------------------- |
| 名称 | `@CQMetaEvent` |
| 触发前提 | 当 `post_type` 为 `meta_event` 时触发(元事件上报时) |
| 命名空间 | `ZM\Annotation\CQ\CQMetaEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无作用 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| --------------- | ------------------ | ------------------------------------------------------------ | ---- |
| meta_event_type | `string`**必需** | 限定元事件的类型,见 [OneBot - 元事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/meta.md) | |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
TODO先放着有时间再更。
## CQBefore()
所有机器人事件的前置注解事件,一般用作消息过滤、全局日志、全局替换等。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------------------------ |
| 名称 | `@CQBefore` |
| 触发前提 | 当 `post_type` 等于参数 `cq_event` 时触发 |
| 命名空间 | `ZM\Annotation\CQ\CQBefore` |
| 适用位置 | 方法 |
| 返回值处理 | 仅可返回 `bool`,如果为 `false`,则阻断 `cq_event` 类的所有事件防止被执行 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | ------------------------------------------------------------ | ------------------------ | ---- |
| cq_event | `string`**必需**,支持 `message``notice``request``meta_event` | 限定机器人时间的类型 | |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
=== "代码"
```php
<?php
namespace Module\Example;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQMessage;
class Test {
/**
* @CQBefore("message")
*/
public function filter(){
// 可用于敏感词,如政治相关的词语不响应其他模块
if(mb_strpos(ctx()->getMessage(), "谷歌") !== false) return false;
else return true;
}
/**
* @CQCommand("百科")
*/
public function wiki() {
$content = ctx()->getNextArg("请说你要查百科的内容");
// 这里假设你对接了一个查百科的接口
return "已搜到匹配 $content 的如下结果:".FakeAPI::searchWiki($content);
}
}
```
=== "效果"
<chat-box>
) 百科 北京
( 已搜到匹配 北京 的如下结果blablabla
) 百科 谷歌被封
^ 机器人没有任何回复
!!! warning "注意"
在设置了 `level` 参数后,如果设置了多个 `@CQBefore` 监听事件函数,更高 `level` 的事件函数返回了 `false`,则低 `level` 的绑定函数不会执行,所有 `@CQMessage` 绑定的事件也不会执行。
你也可以使用 `@CQBefore` 做一些消息的转发和过滤。比如你想去除用户发来的文字中的 emoji、图片等 CQ 码,只保留文本。
## CQAfter()
同上。只是在以上所有事件都调用后才会调用的。
## CQAPIResponse()
TODO还没写完先放着有时间再更。

View File

@@ -0,0 +1,231 @@
# 路由注解事件
炸毛框架提供了一个简易但是高效易用的 HTTP 路由注解,你可以使用路由功能来开发任何 Web 应用微服务、API 接口、中间件等。
!!! quote "开发提示"
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不罗嗦。
## 控制器和路由
Controller 和 Route 为路由注解事件的核心注解事件,其中 Controller 的注解事件为 `@Controller`Route 的注解事件为 `@RequestMapping`
### Controller()
#### 属性
| 类型 | 值 |
| ---------- | ------------------------------- |
| 名称 | `@Controller` |
| 触发前提 | 当路由 url 匹配到时进入触发 |
| 命名空间 | `ZM\Annotation\Http\Controller` |
| 适用位置 | 类 |
| 返回值处理 | 对类注解修饰,无返回值 |
#### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | -------------- | ------------ | ---- |
| prefix | `string`,必需 | 控制器的 url | 空 |
### RequestMapping()
#### 属性
| 类型 | 值 |
| ---------- | ----------------------------------------------------------- |
| 名称 | `@RequestMapping` |
| 触发前提 | 当路由 url 匹配到时进入触发 |
| 命名空间 | `ZM\Annotation\Http\RequestMapping` |
| 适用位置 | 方法 |
| 返回值处理 | 返回类型是 `string` 时,自动调用 HTTP 响应并返回 200 状态码 |
#### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------------- | ----------------------------------------------------------- | -------------------------- | ------------------------------------------ |
| route | `string`,必需 | 控制器的 url | 空 |
| name | `string` | 路由的名称 | 空 |
| request_method | `array`,限定 `RequestMethod::GET` 等常量 | 限制激活路由的 HTTP 方法 | `[RequestMethod::GET,RequestMethod::POST]` |
| params | `array`,当路由中含有如 `{id}` 类似的动态路由时,会动态改变 | 动态参数的路由参数值的绑定 | `[]` |
#### 函数调用参数
- `$param`:如果路由中存在变量(动态路由),则会把动态路由所匹配的参数放入 `$param` 数组中。
```php
/**
* @RequestMapping(route="/test/{ass}")
*/
public function testName($param) {
return "Your name is ".($param["ass"] ?? "unknown");
}
```
### 路由示例
=== "代码"
```php
<?php
namespace Module\Example;
use ZM\Annotation\Http\Controller;
use ZM\Annotation\Http\RequestMapping;
/**
* @Controller("/api")
*/
class Hello {
/**
* @RequestMapping("/index")
*/
public function index(){
ctx()->getResponse()->end("This is API index page"); // 使用上下文获取响应对象
}
/**
* @RequestMapping("/ping")
*/
public function ping(){
return "pong"; // 直接返回字符串
}
}
```
=== "效果"
!!! example "效果描述"
当访问浏览器的 `http://localhost:20001/api/index` 时,浏览器会返回 `This is API index page`,当访问 `/api/ping` 的 url 时,浏览器会返回 `pong`。
```
/ -> 无任何路由
/api/index -> Hello->index
/api/ping -> Hello->ping
```
!!! tip "提示"
当 `@Controller` 为 `/` 的时候,效果和不写是一样的,`@RequestMapping` 为 `/` 或 `/index/inside` 等多级路由也是可以的。
### 绑定参数
在 `@RequestMapping` 中,不仅可以写静态的路由地址,也可以写绑定的参数。例如:`@RequestMapping(route="/index/{name}")`,则访问 `/index/xxx` 的时候,你在函数方法内可以这样获取此参数:
```php
/**
* @RequestMapping("/index/{name}")
*/
public function index($arg) {
return "Your param 'name' is ".$arg["name"];
}
```
## 获取请求参数 GET / POST
炸毛框架支持获取外部 HTTP 请求进来的 GET 和 POST 请求,通过获取 HTTP 请求对象 [Request](/advanced/inside-class/) 即可。对象具体属性和方法点这个链接进去就行。
### 示例
=== "获取 GET"
```php
/**
* @RequestMapping("/testUrl")
*/
public function testUrl() {
$get = ctx()->getRequest()->get;
if(isset($get["name"])) return "hello, ".$get["name"];
else return "Unknown name!!";
}
```
=== "获取 POSTx-www-form-urlencoded"
```php
/**
* @RequestMapping("/testUrl")
*/
public function testUrl() {
$post = ctx()->getRequest()->post;
if(isset($post["name"])) return "hello, ".$post["name"];
else return "Unknown name!!";
}
```
=== "获取 JSON POST"
```php
/**
* @RequestMapping("/testUrl")
*/
public function testUrl() {
$post = ctx()->getRequest()->rawContent();
$json = json_decode($post, true);
if ($json === null) return "Invalid json data!";
if(isset($json["name"])) return "hello, ".$json["name"];
else return "Unknown name!!";
}
```
## 设置路由请求方式
如果想要设置允许请求控制器的 HTTP 请求方式,可以使用方法在控制器中的 `@RequestMapping` 注解配置 `method` 参数,可以是 `GET``POST``PUT`, `PATCH``DELETE``OPTIONS``HEAD` 中的一个或多个。
- 限定 HTTP 方法:`@RequestMapping(method="GET")``@RequestMapping(method={"GET","POST"})`
## 静态文件服务器
框架支持了静态文件的访问。如需使用,则需要先到配置文件中配置相应的 `static_file_server` 参数中 `status` 为 `true`。
框架分为两种静态文件服务器,一种是全局的静态文件服务器,比如框架部署在 `http://127.0.0.1:20001/` 上通过 HTTP 访问,如果没有访问到 `@RequestMapping` 注解事件注册的路由地址,则会通过 url 自动查找静态文件服务器设置的根路径下面的文件,如果都不存在则会返回 404。
### 配置全局静态文件服务器
我们假设在你写的框架应用的根目录下,有如下文件和内容:
```
resources/html/hello.html (下面是内容)
<html>
<head>
<meta charset="utf-8">
</head>
<body>
框架文档内容太多了,写不完!!!
</body>
</html>
```
然后在 `global.php` 配置文件中静态文件服务器参数为:
```php
/** 静态文件访问 */
$config['static_file_server'] = [
'status' => true,
'document_root' => realpath(__DIR__ . "/../") . '/resources/html',
'document_index' => [
'index.html'
]
];
```
最终,我们通过 `vendor/bin/start server` 等方式,启动框架后,浏览器访问 `http://127.0.0.1:20001/hello.html` 即可获取内容。
### 配置局部静态文件服务器
所涉及的类的命名空间:`use ZM\Http\StaticFileHandler;`
局部静态文件服务器一般用于,比如机器人要发送图片,或者给其他 HTTP 服务提供文件下载的接口时可用。我们假设写了一个图片收集的一个静态文件夹区域,将其中一个子路由当作图片静态目录:
```php
/**
* @RequestMapping("/images/{filename}")
* @param $param
* @return StaticFileHandler
*/
public function staticImage($param) {
Console::info("[下载图片] " . $param["filename"]);
return new StaticFileHandler($param["filename"], "/path/to/your/image_dir/");
}
```
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。

View File

@@ -20,6 +20,7 @@
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
| `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` |
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` |
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
| `access_token` | OneBot 客户端连接约定的token留空则无 | 空 |
@@ -82,6 +83,12 @@
| `document_root` | 静态文件的根目录 | `{WORKING_DIR}/resources/html` |
| `document_index` | 默认索引的文件名列表 | `["index.html"]` |
### 子表 worker_cache
| 配置名称 | 说明 | 默认值 |
| -------- | --------------------------- | ------ |
| `worker` | 跨进程缓存的存储工作进程 id | 0 |
## 多环境下的配置文件
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境production / development / staging来编写配置文件。

View File

@@ -61,12 +61,12 @@ pecl install swoole
如果你是通过**主机安装 PHP 部署的环境**,下方是通过脚手架来构建项目的命令行。
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter/
composer update
composer create-project zhamao/framework-starter zhamao-app
cd zhamao-app/ # 这个是你可以自己定义的名称
vendor/bin/start server # 启动框架
```
如果是通过 **Docker 部署的环境**,则需要在先克隆脚手架后在文件夹内使用 Docker 命令下的 `composer update`
如果是通过 **Docker 部署的环境**,则需要在先克隆脚手架后在文件夹内使用 Docker 命令下的 `composer update`(如果主机环境有 composer 也可以使用 `composer create-project` 的方式拉取脚手架。)
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git

View File

@@ -0,0 +1,10 @@
# 快速上手 - HTTP 服务器篇
HTTP 服务器篇主要讲解如何通过炸毛框架来实现微服务、API 通用接口等等这些东西的。
- [HTTP 服务器 - 路由和静态文件篇](/event/route-annotations/)
- [HTTP 服务器 - 存储 - LightCache 轻量缓存](/component/light-cache/)
- [HTTP 服务器 - 存储 - Redis](/component/redis/)
- [HTTP 服务器 - 存储 - MySQL](/component/mysql/)
- [HTTP 客户端](/component/zmrequest/)

View File

@@ -1,4 +0,0 @@
# 快速上手 - HTTP 服务器篇
HTTP 服务器篇暂时先放一放,大家应该主要都是奔着机器人开发来的吧~

View File

@@ -33,7 +33,7 @@ public function index() {
首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
1. Linux 命令行基础
1. Linux 命令行(会跑 Linux 程序)
2. php 7.2+ 开发环境
3. HTTP 协议(可选)
4. OneBot 机器人聊天接口标准(可选)

View File

@@ -46,20 +46,25 @@ function setCookie(name, value) {
}
s_theme=getCookie("_theme");
if(s_theme === undefined) s_theme = "default";
document.body.setAttribute("data-md-color-scheme", s_theme)
var name = document.querySelector("#__code_0 code span:nth-child(7)")
name.textContent = s_theme
if(s_theme !== undefined) {
document.body.setAttribute("data-md-color-scheme", s_theme)
var name = document.querySelector("#__code_0 code span:nth-child(7)")
name.textContent = s_theme
}
s_primary=getCookie("_primary_color");
document.body.setAttribute("data-md-color-primary", s_primary);
var name2 = document.querySelector("#__code_2 code span:nth-child(7)");
if(s_primary !== null && name2 !== null) name2.textContent = s_primary.replace("-", " ");
if(s_primary !== null) {
document.body.setAttribute("data-md-color-primary", s_primary);
var name2 = document.querySelector("#__code_2 code span:nth-child(7)");
if (s_primary !== null && name2 !== null) name2.textContent = s_primary.replace("-", " ");
}
s_accent=getCookie("_accent_color");
document.body.setAttribute("data-md-color-accent", s_accent);
var name3 = document.querySelector("#__code_3 code span:nth-child(7)");
if(s_accent !== null && name3 !== null) name3.textContent = s_accent.replace("-", " ");
if(s_accent !== null) {
document.body.setAttribute("data-md-color-accent", s_accent);
var name3 = document.querySelector("#__code_3 code span:nth-child(7)");
if (s_accent !== null && name3 !== null) name3.textContent = s_accent.replace("-", " ");
}
setTimeout(() => {
let ls = document.querySelectorAll("chat-box");
@@ -70,13 +75,20 @@ setTimeout(() => {
if(j === '') continue;
if(j.substr(0, 2) === ') ') {
final += '<div class="doc-chat-row">\n' +
' <div class="doc-chat-box">' + j.substr(2) + '</div>\n' +
' <div class="doc-chat-box">' + j.substr(2).replaceAll("\\n", "<br>") + '</div>\n' +
' <img class="doc-chat-avatar" src="http://api.btstu.cn/sjtx/api.php" alt=""/>\n' +
' </div>';
} else if (j.substr(0, 2) === '( ') {
final += '<div class="doc-chat-row doc-chat-row-robot">\n' +
' <img class="doc-chat-avatar" src="https://docs-v1.zhamao.xin/logo.png" alt=""/>\n' +
' <div class="doc-chat-box doc-chat-box-robot">' + j.substr(2) + '</div>\n' +
' <div class="doc-chat-box doc-chat-box-robot">' + j.substr(2).replaceAll("\\n", "<br>") + '</div>\n' +
' </div>';
} else if (j.substr(0, 2) === '^ ') {
final += '<div class="doc-chat-row doc-chat-banner">' + j.substr(2) + '</div>';
} else if (j.substr(0, 2) === '[ ') {
final += '<div class="doc-chat-row doc-chat-row-robot">\n' +
' <img class="doc-chat-avatar" src="https://docs-v1.zhamao.xin/logo.png" alt=""/>\n' +
' <div class="doc-chat-box doc-chat-box-robot"><img src="' + j.substr(2) + '" alt=""/></div>\n' +
' </div>';
}
}

View File

@@ -1,3 +1,91 @@
# 更新日志v2 版本)
> 暂未发布正式版。
## v2.2.0
> 更新时间2021.1.29
- 新增:`@OnPipeMessageEvent` 注解
- 新增:进程管理器
- 新增:`--daemon` 守护进程化后查看状态以及一系列操作的命令行
- 新增WorkerCache
- 修复:路由问题
- 修复:`http_header` 配置项不生效的 bug
- 优化:框架内部所有异常全部基于 `ZMException`
- 优化SingletonTrait 支持扩展
## v2.1.6
> 更新时间2021.1.18
- 优化:代码结构
- 增加:更多提示语
- 修复:处理空格消息时的报错
- 修复上下文的bug
## v2.1.5
> 更新时间2021.1.13
- 优化:终端对 PHP Warning 和 PHP Notice 的报错信息显示,统一格式
- 新增:`ctx()->getNumArg()` 上下文中快速获取数字类型的参数的方法
- 优化:删除不必要的调试信息
- 优化:路由组件全面替换为 `symfony/routing`,兼容性和稳定性 up
## v2.1.4
> 更新时间2021.1.3
- 修复:启动时会提示丢失类的 bug
- 优化HTTP 响应类如果被使用了则一律返回 false
- 优化PHP Warning 等报错统一样式
## v2.1.3
> 更新时间2021.1.2
- 修复:注解解析器在某种特殊情况下导致的 bug
## v2.1.2
> 更新时间2021.1.2
- 修复:引入包模式启动时会导致的满屏报错
## v2.1.1
> 更新时间2021.1.2
- 修复:自定义加载注解选定 composer.json 文件错误的 bug
## v2.1.0
> 更新时间2021.1.2
- 新增:`@OnOpenEvent``@OnCloseEvent``@OnMessageEvent``@OnRequestEvent`
- 优化事件分发器,修复一些事件分发过程中的 bug
- 修复 `@CQBefore` 事件的 bug
## v2.0.3
> 更新时间2020.12.31
- 修复CQBefore 注解事件在 level 低于 200 时无法调用的 bug
- 修复CQMetaEvent 注解事件调用时报错的 bug
## v2.0.2
> 更新时间2020.12.31
- 更新:将 CQ 码调用类更新到与最新 OneBot 标准相兼容的状态
## v2.0.1
> 更新时间2020.12.23
- 修复:开屏报错文件夹不存在
## v2.0
> 更新时间2020.12.23
已发布正式版。

View File

@@ -2,13 +2,16 @@ site_name: 炸毛框架 v2
repo_name: '炸毛框架'
repo_url: 'https://github.com/zhamao-robot/zhamao-framework'
edit_uri: 'blob/2.0-dev/docs/'
edit_uri: 'blob/master/docs/'
theme:
name: material
logo: assets/logos.png
favicon: assets/favicon.png
language: zh
palette:
primary: blue
accent: blue
features:
- navigation.tabs
extra_javascript:
@@ -52,22 +55,47 @@ copyright: 'Copyright &copy; 2019 - 2020 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&n
nav:
- 指南:
- 介绍: index.md
- 安装框架: guide/安装.md
- 快速上手(机器人篇): guide/快速上手-机器人.md
- 快速上手HTTP篇: guide/快速上手-http.md
- 选择聊天机器人实例: guide/OneBot实例.md
- 基本配置: guide/基本配置.md
- 编写模块: guide/编写模块.md
- 注册事件响应: guide/注册事件响应.md
- 安装框架: guide/installation.md
- 快速上手(机器人篇): guide/quickstart-robot.md
- 快速上手HTTP篇: guide/quickstart-http.md
- 选择聊天机器人实例: guide/onebot-choose.md
- 基本配置: guide/basic-config.md
- 编写模块: guide/write-module.md
- 注册事件响应: guide/register-event.md
- 事件和注解:
- 事件和注解: event/index.md
- 机器人注解事件: event/robot-annotations.md
- HTTP 路由注解事件: event/route-annotations.md
- 框架核心注解事件: event/framework-annotations.md
- 中间件注解: event/middleware.md
- 自定义注解: event/custom-annotations.md
- 事件分发器: event/event-dispatcher.md
- 框架组件:
- 框架组件: component/index.md
- 机器人 API: component/robot-api.md
- CQ 码(多媒体消息): component/cqcode.md
- 上下文: component/context.md
- 存储:
- LightCache 轻量缓存: component/light-cache.md
- MySQL 数据库: component/mysql.md
- Redis 数据库: component/redis.md
- ZMAtomic 原子计数器: component/atomics.md
- SpinLock 自旋锁: component/spin-lock.md
- 协程池: component/coroutine-pool.md
- 单例类: component/singleton-trait.md
- ZMUtil 杂项: component/zmutil.md
- 全局方法: component/global-functions.md
- HTTP 和 WebSocket 客户端: component/zmrequest.md
- Console 终端: component/console.md
- 进阶开发:
- 进阶开发: advanced/index.md
- 框架剖析: advanced/framework-structure.md
- 框架启动模式: advanced/custom-start.md
- 从 v1 升级: advanced/to-v2.md
- FAQ:
- FAQ: FAQ.md
- 内部类文件手册: advanced/inside-class.md
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
- 框架多进程: advanced/multi-process.md
- FAQ: FAQ.md
- 更新日志:
- 更新日志v2: update/v2.md
- 更新日志v1: update/v1.md

View File

@@ -1,4 +1,4 @@
<?php
<?php #plain
//这里写你的全局函数
function pgo(callable $func, $name = "default") {

View File

@@ -3,13 +3,14 @@
namespace Module\Example;
use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\RequestMapping;
use ZM\Event\EventDispatcher;
use ZM\Store\Redis\ZMRedis;
use ZM\Utils\ZMUtil;
/**
@@ -19,29 +20,6 @@ use ZM\Utils\ZMUtil;
*/
class Hello
{
/**
* 一个简单的redis连接池使用demo将下方user_id改为你自己的QQ号即可(为了不被不法分子利用)
* @CQCommand("redis_test",user_id=627577391)
*/
public function testCase() {
$a = new ZMRedis();
$redis = $a->get();
$r1 = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想设置的操作[r/w]");
switch ($r1) {
case "r":
$k = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想读取的键名");
$result = $redis->get($k);
ctx()->reply("结果:" . $result);
break;
case "w":
$k = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想写入的键名");
$v = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想写入的字符串");
$result = $redis->set($k, $v);
ctx()->reply("结果:" . ($result ? "成功" : "失败"));
break;
}
}
/**
* 使用命令 .reload 发给机器人远程重载,注意将 user_id 换成你自己的 QQ
* @CQCommand(".reload",user_id=627577391)
@@ -77,9 +55,9 @@ class Hello
*/
public function randNum() {
// 获取第一个数字类型的参数
$num1 = ctx()->getArgs(ZM_MATCH_NUMBER, "请输入第一个数字");
$num1 = ctx()->getNumArg("请输入第一个数字");
// 获取第二个数字类型的参数
$num2 = ctx()->getArgs(ZM_MATCH_NUMBER, "请输入第二个数字");
$num2 = ctx()->getNumArg("请输入第二个数字");
$a = min(intval($num1), intval($num2));
$b = max(intval($num1), intval($num2));
// 回复用户结果
@@ -111,12 +89,12 @@ class Hello
* @return string
*/
public function paramGet($param) {
return "Your name: {$param["name"]}";
return "Hello, ".$param["name"];
}
/**
* 在机器人连接后向终端输出信息
* @OnSwooleEvent("open",rule="connectIsQQ()")
* @OnOpenEvent("qq")
* @param $conn
*/
public function onConnect(ConnectionObject $conn) {
@@ -125,7 +103,7 @@ class Hello
/**
* 在机器人断开连接后向终端输出信息
* @OnSwooleEvent("close",rule="connectIsQQ()")
* @OnCloseEvent("qq")
* @param ConnectionObject $conn
*/
public function onDisconnect(ConnectionObject $conn) {
@@ -134,7 +112,7 @@ class Hello
/**
* 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰
* @OnSwooleEvent("request",rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
* @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
*/
public function onRequest() {
EventDispatcher::interrupt();
@@ -142,7 +120,7 @@ class Hello
/**
* 框架会默认关闭未知的WebSocket链接因为这个绑定的事件你可以根据你自己的需求进行修改
* @OnSwooleEvent(type="open",rule="connectIsDefault()")
* @OnOpenEvent("default")
*/
public function closeUnknownConn() {
Console::info("Unknown connection , I will close it.");

View File

@@ -2,6 +2,7 @@
namespace Module\Middleware;
use Exception;
use ZM\Annotation\Http\HandleAfter;
use ZM\Annotation\Http\HandleBefore;
use ZM\Annotation\Http\HandleException;
@@ -37,8 +38,11 @@ class TimerMiddleware implements MiddlewareInterface
/**
* @HandleException(\Exception::class)
* @param Exception $e
* @throws Exception
*/
public function onException() {
public function onException(Exception $e) {
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred.");
throw $e;
}
}

View File

@@ -34,70 +34,59 @@ class CQ
return " ";
}
/**
* 发送emoji表情
* @param $id
* @return string
*/
public static function emoji($id) {
if (is_numeric($id)) {
return "[CQ:emoji,id=" . $id . "]";
}
Console::warning("传入的emoji id($id)错误!");
return " ";
}
/**
* 发送原创表情存放在酷Q目录的data/bface/下
* @param $id
* @return string
*/
public static function bface($id) {
return "[CQ:bface,id=" . $id . "]";
}
/**
* 发送小表情
* @param $id
* @return string
*/
public static function sface($id) {
if (is_numeric($id)) {
return "[CQ:sface,id=" . $id . "]";
}
Console::warning("传入的sface id($id)错误!");
return " ";
}
/**
* 发送图片
* cache为<FALSE>时禁用CQ-HTTP-API插件的缓存
* @param $file
* @param bool $cache
* @param bool $flash
* @param bool $proxy
* @param int $timeout
* @return string
*/
public static function image($file, $cache = true) {
if ($cache === false)
return "[CQ:image,file=" . $file . ",cache=0]";
else
return "[CQ:image,file=" . $file . "]";
public static function image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1) {
return
"[CQ:image,file=" . $file .
(!$cache ? ",cache=0" : "") .
($flash ? ",type=flash" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . $timeout) : "") .
"]";
}
/**
* 发送语音
* cache为<FALSE>时禁用CQ-HTTP-API插件的缓存
* magic为<TRUE>时标记为变声
* @param $file
* @param bool $magic
* @param bool $cache
* @param bool $proxy
* @param int $timeout
* @return string
*/
public static function record($file, $magic = false, $cache = true) {
if ($cache === false) $c = ",cache=0";
else $c = "";
if ($magic === true) $m = ",magic=true";
else $m = "";
return "[CQ:record,file=" . $file . $c . $m . "]";
public static function record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1) {
return
"[CQ:record,file=" . $file .
(!$cache ? ",cache=0" : "") .
($magic ? ",magic=1" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . $timeout) : "") .
"]";
}
/**
* 发送短视频
* @param $file
* @param bool $cache
* @param bool $proxy
* @param int $timeout
* @return string
*/
public static function video($file, $cache = true, $proxy = true, $timeout = -1) {
return
"[CQ:video,file=" . $file .
(!$cache ? ",cache=0" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . $timeout) : "") .
"]";
}
/**
@@ -124,6 +113,56 @@ class CQ
return "[CQ:shake]";
}
/**
* 发送新的戳一戳
* @param $type
* @param $id
* @param string $name
* @return string
*/
public static function poke($type, $id, $name = "") {
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? ",name=$name" : "") . "]";
}
/**
* 发送匿名消息
* @param int $ignore
* @return string
*/
public static function anonymous($ignore = 1) {
return "[CQ:anonymous".($ignore != 1 ? ",ignore=0" : "")."]";
}
/**
* 发送链接分享(只能在单条回复中单独使用)
* @param $url
* @param $title
* @param null $content
* @param null $image
* @return string
*/
public static function share($url, $title, $content = null, $image = null) {
if ($content === null) $c = "";
else $c = ",content=" . $content;
if ($image === null) $i = "";
else $i = ",image=" . $image;
return "[CQ:share,url=" . $url . ",title=" . $title . $c . $i . "]";
}
/**
* 发送好友或群推荐名片
* @param $type
* @param $id
* @return string
*/
public static function contact($type, $id) {
return "[CQ:contact,type=$type,id=$id]";
}
public static function location($lat, $lon, $title = "", $content = "") {
}
/**
* 发送音乐分享(只能在单条回复中单独使用)
* qq、163、xiami为内置分享需要先通过搜索功能获取id后使用
@@ -136,10 +175,10 @@ class CQ
* $image 为音乐卡片的图片链接地址(可忽略)
* @param $type
* @param $id_or_url
* @param string $audio
* @param string $title
* @param string $content
* @param string $image
* @param null $audio
* @param null $title
* @param null $content
* @param null $image
* @return string
*/
public static function music($type, $id_or_url, $audio = null, $title = null, $content = null, $image = null) {
@@ -164,20 +203,12 @@ class CQ
}
}
/**
* 发送链接分享(只能在单条回复中单独使用)
* @param $url
* @param $title
* @param null $content
* @param null $image
* @return string
*/
public static function share($url, $title, $content = null, $image = null) {
if ($content === null) $c = "";
else $c = ",content=" . $content;
if ($image === null) $i = "";
else $i = ",image=" . $image;
return "[CQ:share,url=" . $url . ",title=" . $title . $c . $i . "]";
public static function forward($id) {
return "[CQ:forward,id=$id]";
}
public static function node($user_id, $nickname, $content) {
return "[CQ:node,user_id=$user_id,nickname=$nickname,content=".self::escape($content)."]";
}
/**

View File

@@ -12,6 +12,7 @@ use ReflectionMethod;
use ZM\Annotation\Http\{HandleAfter, HandleBefore, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping};
use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Module\Closed;
use ZM\Http\RouteManager;
use ZM\Utils\DataProvider;
class AnnotationParser
@@ -33,7 +34,7 @@ class AnnotationParser
*/
public function __construct() {
$this->start_time = microtime(true);
$this->loadAnnotationClasses();
//$this->loadAnnotationClasses();
$this->req_mapping[0] = [
'id' => 0,
'pid' => -1,
@@ -88,7 +89,7 @@ class AnnotationParser
//预处理1将适用于每一个函数的注解到类注解重新注解到每个函数下面
if ($vs instanceof ErgodicAnnotation) {
foreach ($this->annotation_map[$v]["methods"] as $method) {
foreach (($this->annotation_map[$v]["methods"] ?? []) as $method) {
$copy = clone $vs;
$copy->method = $method->getName();
$this->annotation_map[$v]["methods_annotations"][$method->getName()][] = $copy;
@@ -100,20 +101,20 @@ class AnnotationParser
unset($this->annotation_map[$v]);
continue 2;
} elseif ($vs instanceof MiddlewareClass) {
Console::verbose("正在注册中间件 " . $reflection_class->getName());
Console::debug("正在注册中间件 " . $reflection_class->getName());
$rs = $this->registerMiddleware($vs, $reflection_class);
$this->middlewares[$rs["name"]] = $rs;
}
}
//预处理3处理每个函数上面的特殊注解就是需要操作一些东西的
foreach ($this->annotation_map[$v]["methods_annotations"] as $method_name => $methods_annotations) {
foreach (($this->annotation_map[$v]["methods_annotations"] ?? []) as $method_name => $methods_annotations) {
foreach ($methods_annotations as $method_anno) {
/** @var AnnotationBase $method_anno */
$method_anno->class = $v;
$method_anno->method = $method_name;
if ($method_anno instanceof RequestMapping) {
$this->registerRequestMapping($method_anno, $method_name, $v, $methods_annotations); //TODO: 用symfony的routing重写
RouteManager::importRouteByAnnotation($method_anno, $method_name, $v, $methods_annotations);
} elseif ($method_anno instanceof Middleware) {
$this->middleware_map[$method_anno->class][$method_anno->method][] = $method_anno->middleware;
}
@@ -121,11 +122,6 @@ class AnnotationParser
}
}
}
//预处理4生成路由树换成symfony后就不需要了
$tree = $this->genTree($this->req_mapping);
$this->req_mapping = $tree[0];
Console::debug("解析注解完毕!");
}
@@ -135,11 +131,11 @@ class AnnotationParser
public function generateAnnotationEvents() {
$o = [];
foreach ($this->annotation_map as $module => $obj) {
foreach ($obj["class_annotations"] as $class_annotation) {
foreach (($obj["class_annotations"] ?? []) as $class_annotation) {
if ($class_annotation instanceof ErgodicAnnotation) continue;
else $o[get_class($class_annotation)][] = $class_annotation;
}
foreach ($obj["methods_annotations"] as $method_name => $methods_annotations) {
foreach (($obj["methods_annotations"] ?? []) as $method_name => $methods_annotations) {
foreach ($methods_annotations as $annotation) {
$o[get_class($annotation)][] = $annotation;
}
@@ -175,109 +171,6 @@ class AnnotationParser
//private function below
private function registerRequestMapping(RequestMapping $vss, $method, $class, $methods_annotations) {
$prefix = '';
foreach ($methods_annotations as $annotation) {
if ($annotation instanceof Controller) {
$prefix = $annotation->prefix;
break;
}
}
$array = $this->req_mapping;
$uid = count($array);
$prefix_exp = explode("/", $prefix);
$route_exp = explode("/", $vss->route);
foreach ($prefix_exp as $k => $v) {
if ($v == "" || $v == ".." || $v == ".") {
unset($prefix_exp[$k]);
}
}
foreach ($route_exp as $k => $v) {
if ($v == "" || $v == ".." || $v == ".") {
unset($route_exp[$k]);
}
}
if ($prefix_exp == [] && $route_exp == []) {
$array[0]['method'] = $method;
$array[0]['class'] = $class;
$array[0]['request_method'] = $vss->request_method;
$array[0]['route'] = $vss->route;
$this->req_mapping = $array;
return;
}
$pid = 0;
while (($shift = array_shift($prefix_exp)) !== null) {
foreach ($array as $k => $v) {
if ($v["name"] == $shift && $pid == ($v["pid"] ?? -1)) {
$pid = $v["id"];
continue 2;
}
}
$array[$uid++] = [
'id' => $uid - 1,
'pid' => $pid,
'name' => $shift
];
$pid = $uid - 1;
}
while (($shift = array_shift($route_exp)) !== null) {
/*if (mb_substr($shift, 0, 1) == "{" && mb_substr($shift, -1, 1) == "}") {
$p->removeAllRoute();
Console::info("移除本节点其他所有路由中");
}*/
foreach ($array as $k => $v) {
if ($v["name"] == $shift && $pid == ($v["pid"] ?? -1)) {
$pid = $v["id"];
continue 2;
}
}
if (mb_substr($shift, 0, 1) == "{" && mb_substr($shift, -1, 1) == "}") {
foreach ($array as $k => $v) {
if ($pid == $v["id"]) {
$array[$k]["param_route"] = $uid;
}
}
}
$array[$uid++] = [
'id' => $uid - 1,
'pid' => $pid,
'name' => $shift
];
$pid = $uid - 1;
}
$array[$uid - 1]['method'] = $method;
$array[$uid - 1]['class'] = $class;
$array[$uid - 1]['request_method'] = $vss->request_method;
$array[$uid - 1]['route'] = $vss->route;
$this->req_mapping = $array;
}
/** @noinspection PhpIncludeInspection */
private function loadAnnotationClasses() {
$class = getAllClasses(WORKING_DIR . "/src/ZM/Annotation/", "ZM\\Annotation");
foreach ($class as $v) {
$s = WORKING_DIR . '/src/' . str_replace("\\", "/", $v) . ".php";
//Console::debug("Requiring annotation " . $s);
require_once $s;
}
$class = getAllClasses(DataProvider::getWorkingDir() . "/src/Custom/Annotation/", "Custom\\Annotation");
foreach ($class as $v) {
$s = DataProvider::getWorkingDir() . '/src/' . str_replace("\\", "/", $v) . ".php";
Console::debug("Requiring custom annotation " . $s);
require_once $s;
}
}
private function genTree($items) {
$tree = array();
foreach ($items as $item)
if (isset($items[$item['pid']]))
$items[$item['pid']]['son'][] = &$items[$item['id']];
else
$tree[] = &$items[$item['id']];
return $tree;
}
private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class) {
$result = [
"class" => "\\" . $reflection_class->getName(),
@@ -297,7 +190,7 @@ class AnnotationParser
return $result;
}
private function sortByLevel(&$events, string $class_name, $prefix = "") {
public function sortByLevel(&$events, string $class_name, $prefix = "") {
if (is_a($class_name, Level::class, true)) {
$class_name .= $prefix;
usort($events[$class_name], function ($a, $b) {

View File

@@ -21,8 +21,6 @@ class CQMetaEvent extends AnnotationBase implements Level
* @Required()
*/
public $meta_event_type = '';
/** @var string */
public $sub_type = '';
/** @var int */
public $level;

View File

@@ -0,0 +1,22 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\Interfaces\Rule;
/**
* @Annotation
* @Target("METHOD")
* Class OnCloseEvent
* @package ZM\Annotation\Swoole
*/
class OnCloseEvent extends OnSwooleEventBase
{
/**
* @var string
*/
public $connect_type = "default";
}

View File

@@ -0,0 +1,21 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* @Annotation
* @Target("METHOD")
* Class OnMessageEvent
* @package ZM\Annotation\Swoole
*/
class OnMessageEvent extends OnSwooleEventBase
{
/**
* @var string
*/
public $connect_type = "default";
}

View File

@@ -0,0 +1,21 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* @Annotation
* @Target("METHOD")
* Class OnOpenEvent
* @package ZM\Annotation\Swoole
*/
class OnOpenEvent extends OnSwooleEventBase
{
/**
* @var string
*/
public $connect_type = "default";
}

View File

@@ -0,0 +1,24 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class OnPipeMessageEvent
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("METHOD")
*/
class OnPipeMessageEvent extends AnnotationBase
{
/**
* @var string
* @Required()
*/
public $action;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* @Annotation
* @Target("METHOD")
* Class OnRequestEvent
* @package ZM\Annotation\Swoole
*/
class OnRequestEvent extends OnSwooleEventBase
{
}

View File

@@ -10,7 +10,7 @@ use ZM\Annotation\AnnotationBase;
* Class OnWorkerStart
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("ALL")
* @Target("METHOD")
*/
class OnStart extends AnnotationBase
{

View File

@@ -5,17 +5,14 @@ namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Interfaces\Rule;
/**
* Class OnSwooleEvent
* @Annotation
* @Target("ALL")
* @Target("METHOD")
* @package ZM\Annotation\Swoole
*/
class OnSwooleEvent extends AnnotationBase implements Rule, Level
class OnSwooleEvent extends OnSwooleEventBase
{
/**
* @var string
@@ -23,14 +20,6 @@ class OnSwooleEvent extends AnnotationBase implements Rule, Level
*/
public $type;
/** @var string */
public $rule = "";
/** @var int */
public $level = 20;
public $callback = null;
/**
* @return string
*/
@@ -44,33 +33,4 @@ class OnSwooleEvent extends AnnotationBase implements Rule, Level
public function setType(string $type) {
$this->type = $type;
}
/**
* @return string
*/
public function getRule(): string {
return $this->rule;
}
/**
* @param string $rule
*/
public function setRule(string $rule) {
$this->rule = $rule;
}
/**
* @return int
*/
public function getLevel(): int {
return $this->level;
}
/**
* @param int $level
*/
public function setLevel(int $level) {
$this->level = $level;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace ZM\Annotation\Swoole;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Interfaces\Rule;
abstract class OnSwooleEventBase extends AnnotationBase implements Level, Rule
{
/**
* @var string
*/
public $rule = "";
/**
* @var int
*/
public $level = 20;
/**
* @return string
*/
public function getRule(): string {
return $this->rule !== "" ? $this->rule : true;
}
/**
* @param string $rule
*/
public function setRule(string $rule) {
$this->rule = $rule;
}
/**
* @return int
*/
public function getLevel(): int {
return $this->level;
}
/**
* @param int $level
*/
public function setLevel(int $level) {
$this->level = $level;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace ZM\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Utils\DataProvider;
abstract class DaemonCommand extends Command
{
protected $daemon_file = null;
protected function execute(InputInterface $input, OutputInterface $output) {
$pid_path = DataProvider::getWorkingDir() . "/.daemon_pid";
if (!file_exists($pid_path)) {
$output->writeln("<comment>没有检测到正在运行的守护进程!</comment>");
die();
}
$file = json_decode(file_get_contents($pid_path), true);
if ($file === null || posix_getsid(intval($file["pid"])) === false) {
$output->writeln("<comment>未检测到正在运行的守护进程!</comment>");
unlink($pid_path);
die();
}
$this->daemon_file = $file;
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace ZM\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DaemonReloadCommand extends DaemonCommand
{
protected static $defaultName = 'daemon:reload';
protected function configure() {
$this->setDescription("重载守护进程下的用户代码(仅限--daemon模式可用");
}
protected function execute(InputInterface $input, OutputInterface $output) {
parent::execute($input, $output);
system("kill -USR1 " . intval($this->daemon_file["pid"]));
$output->writeln("<info>成功重载!</info>");
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace ZM\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Utils\DataProvider;
class DaemonStatusCommand extends DaemonCommand
{
protected static $defaultName = 'daemon:status';
protected function configure() {
$this->setDescription("查看守护进程框架的运行状态(仅限--daemon模式可用");
}
protected function execute(InputInterface $input, OutputInterface $output) {
parent::execute($input, $output);
$output->writeln("<info>框架运行中pid" . $this->daemon_file["pid"] . "</info>");
$output->writeln("<comment>----- 以下是stdout内容 -----</comment>");
$stdout = file_get_contents($this->daemon_file["stdout"]);
$stdout = explode("\n", $stdout);
for ($i = 10; $i > 0; --$i) {
if (isset($stdout[count($stdout) - $i]))
echo $stdout[count($stdout) - $i] . PHP_EOL;
}
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace ZM\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Utils\DataProvider;
class DaemonStopCommand extends DaemonCommand
{
protected static $defaultName = 'daemon:stop';
protected function configure() {
$this->setDescription("停止守护进程下运行的框架(仅限--daemon模式可用");
}
protected function execute(InputInterface $input, OutputInterface $output) {
parent::execute($input, $output);
system("kill -TERM ".intval($this->daemon_file["pid"]));
unlink(DataProvider::getWorkingDir()."/.daemon_pid");
$output->writeln("<info>成功停止!</info>");
return Command::SUCCESS;
}
}

View File

@@ -74,6 +74,7 @@ class InitCommand extends Command
echo("Error occurred. Please check your updates.\n");
return Command::FAILURE;
}
$output->writeln("<info>Done!</info>");
return Command::SUCCESS;
} elseif (LOAD_MODE === 2) { //从phar启动的框架包初始化的模式
$phar_link = new Phar(__DIR__);

View File

@@ -11,7 +11,6 @@ use ZM\Framework;
class RunServerCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'server';
protected function configure() {
@@ -31,27 +30,17 @@ class RunServerCommand extends Command
]);
$this->setDescription("Run zhamao-framework | 启动框架");
$this->setHelp("直接运行可以启动");
// ...
}
protected function execute(InputInterface $input, OutputInterface $output) {
if(($opt = $input->getOption("env")) !== null) {
if(!in_array($opt, ["production", "staging", "development", ""])) {
if (($opt = $input->getOption("env")) !== null) {
if (!in_array($opt, ["production", "staging", "development", ""])) {
$output->writeln("<error> \"--env\" option only accept production, development, staging and [empty] ! </error>");
return Command::FAILURE;
}
}
// ... put here the code to run in your command
// this method must return an integer number with the "exit status code"
// of the command. You can also use these constants to make code more readable
if (LOAD_MODE == 0) echo "* This is repository mode.\n";
(new Framework($input->getOptions()))->start();
// return this if there was no problem running the command
// (it's equivalent to returning int(0))
return Command::SUCCESS;
// or return this if some error happened during the execution
// (it's equivalent to returning int(1))
// return Command::FAILURE;
}
}

View File

@@ -5,6 +5,9 @@ namespace ZM;
use Exception;
use ZM\Command\DaemonReloadCommand;
use ZM\Command\DaemonStatusCommand;
use ZM\Command\DaemonStopCommand;
use ZM\Command\InitCommand;
use ZM\Command\PureHttpCommand;
use ZM\Command\RunServerCommand;
@@ -40,7 +43,6 @@ class ConsoleApplication extends Application
* @noinspection RedundantSuppression
*/
require_once WORKING_DIR . "/vendor/autoload.php";
echo "* This is repository mode.\n";
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
if (!isset($composer["autoload"]["psr-4"]["Module\\"])) {
echo "框架源码模式需要在autoload文件中添加Module目录为自动加载是否添加[Y/n] ";
@@ -64,6 +66,9 @@ class ConsoleApplication extends Application
}
$this->addCommands([
new DaemonStatusCommand(),
new DaemonReloadCommand(),
new DaemonStopCommand(),
new RunServerCommand(), //运行主服务的指令控制器
new InitCommand(), //初始化用的用于项目初始化和phar初始化
new PureHttpCommand() //纯HTTP服务器指令
@@ -75,6 +80,7 @@ class ConsoleApplication extends Application
if (!($obj instanceof Command)) throw new TypeError("Command register class must be extended by Symfony\\Component\\Console\\Command\\Command");
$this->add($obj);
}*/
return $this;
}
/**

View File

@@ -242,6 +242,14 @@ class Context implements ContextInterface
*/
public function getFullArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_ALL, $prompt_msg); }
/**
* @param string $prompt_msg
* @return int|mixed|string
* @throws InvalidArgumentException
* @throws WaitTimeoutException
*/
public function getNumArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_NUMBER, $prompt_msg); }
public function cloneFromParent() {
set_coroutine_params(self::$context[Co::getPcid()] ?? self::$context[$this->cid]);
return context();

View File

@@ -117,6 +117,8 @@ interface ContextInterface
public function cloneFromParent();
public function getNumArg($prompt_msg = "");
public function copy();
public function getOption();

View File

@@ -5,8 +5,8 @@ namespace ZM\Event;
use Doctrine\Common\Annotations\AnnotationException;
use Error;
use Exception;
use ZM\Annotation\AnnotationBase;
use ZM\Console\Console;
use ZM\Exception\InterruptException;
use ZM\Exception\ZMException;
@@ -17,6 +17,12 @@ use ZM\Utils\ZMUtil;
class EventDispatcher
{
const STATUS_NORMAL = 0; //正常结束
const STATUS_INTERRUPTED = 1; //被interrupt了不管在什么地方
const STATUS_EXCEPTION = 2; //执行过程中抛出了异常
const STATUS_BEFORE_FAILED = 3; //中间件HandleBefore返回了false所以不执行此方法
const STATUS_RULE_FAILED = 4; //判断事件执行的规则函数判定为false所以不执行此方法
/** @var string */
private $class;
/** @var null|callable */
@@ -27,6 +33,10 @@ class EventDispatcher
private $log = false;
/** @var int */
private $eid = 0;
/** @var int */
public $status = self::STATUS_NORMAL;
/** @var mixed */
public $store = null;
/**
* @param null $return_var
@@ -74,41 +84,53 @@ class EventDispatcher
return $this;
}
/**
* @param mixed ...$params
* @throws Exception
* @throws InterruptException
*/
public function dispatchEvents(...$params) {
try {
foreach ((EventManager::$events[$this->class] ?? []) as $v) {
$result = $this->dispatchEvent($v, $this->rule, ...$params);
$this->dispatchEvent($v, $this->rule, ...$params);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。");
if ($result !== false && is_callable($this->return_func)) {
if($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
if (is_callable($this->return_func) && $this->status === self::STATUS_NORMAL) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ...");
($this->return_func)($result);
($this->return_func)($this->store);
}
}
return true;
if($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
} catch (InterruptException $e) {
return $e->return_var;
} catch (AnnotationException $e) {
return false;
$this->store = $e->return_var;
$this->status = self::STATUS_INTERRUPTED;
} catch (Exception $e) {
$this->status = self::STATUS_EXCEPTION;
throw $e;
} catch (Error $e) {
$this->status = self::STATUS_EXCEPTION;
throw $e;
}
}
/**
* @param AnnotationBase|null $v
* @param mixed $v
* @param null $rule_func
* @param mixed ...$params
* @throws AnnotationException
* @throws InterruptException
* @return mixed
* @return bool
*/
public function dispatchEvent(?AnnotationBase $v, $rule_func = null, ...$params) {
public function dispatchEvent($v, $rule_func = null, ...$params) {
$q_c = $v->class;
$q_f = $v->method;
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在判断 " . $q_c . "::" . $q_f . " 方法下的 rule ...");
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在判断 " . $q_c . "::" . $q_f . " 方法下的 ruleFunc ...");
if ($rule_func !== null && !$rule_func($v)) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 rule 判断为 false, 拒绝执行此方法。");
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 ruleFunc 判断为 false, 拒绝执行此方法。");
$this->status = self::STATUS_RULE_FAILED;
return false;
}
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 rule 为真,继续执行方法本身 ...");
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 ruleFunc 为真,继续执行方法本身 ...");
if (isset(EventManager::$middleware_map[$q_c][$q_f])) {
$middlewares = EventManager::$middleware_map[$q_c][$q_f];
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法还绑定了 Middleware" . implode(", ", $middlewares));
@@ -138,7 +160,7 @@ class EventDispatcher
try {
$q_o = ZMUtil::getModInstance($q_c);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
$result = $q_o->$q_f(...$params);
$this->store = $q_o->$q_f(...$params);
} catch (Exception $e) {
if ($e instanceof InterruptException) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 检测到事件阻断调用,正在跳出事件分发器 ...");
@@ -166,13 +188,17 @@ class EventDispatcher
if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 后置事件执行完毕!");
}
}
return $result;
$this->status = self::STATUS_NORMAL;
return true;
}
$this->status = self::STATUS_BEFORE_FAILED;
return false;
} else {
$q_o = ZMUtil::getModInstance($q_c);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
return $q_o->$q_f(...$params);
$this->store = $q_o->$q_f(...$params);
$this->status = self::STATUS_NORMAL;
return true;
}
}
}

View File

@@ -22,6 +22,7 @@ class EventManager
public static function addEvent($event_name, ?AnnotationBase $event_obj) {
self::$events[$event_name][] = $event_obj;
(new AnnotationParser())->sortByLevel(self::$events, $event_name);
}
public static function loadEventByParser(AnnotationParser $parser) {
@@ -37,7 +38,7 @@ class EventManager
public static function registerTimerTick() {
$dispatcher = new EventDispatcher(OnTick::class);
foreach (self::$events[OnTick::class] ?? [] as $vss) {
if (server()->worker_id !== $vss->worker_id) return;
if (server()->worker_id !== $vss->worker_id && $vss->worker_id != -1) return;
//echo server()->worker_id.PHP_EOL;
$plain_class = $vss->class;
Console::debug("Added Middleware-based timer: " . $plain_class . " -> " . $vss->method);

View File

@@ -1,4 +1,6 @@
<?php /** @noinspection PhpComposerExtensionStubsInspection */
<?php /** @noinspection PhpUnreachableStatementInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
namespace ZM\Event;
@@ -9,13 +11,18 @@ use Error;
use Exception;
use PDO;
use ReflectionException;
use Swoole\Coroutine;
use Swoole\Database\PDOConfig;
use Swoole\Database\PDOPool;
use Swoole\Event;
use Swoole\Process;
use Swoole\Timer;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnMessageEvent;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnPipeMessageEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Config\ZMConfig;
@@ -30,12 +37,14 @@ use ZM\Context\Context;
use ZM\Context\ContextInterface;
use ZM\DB\DB;
use ZM\Exception\DbException;
use ZM\Exception\InterruptException;
use ZM\Framework;
use ZM\Http\Response;
use ZM\Module\QQBot;
use ZM\Store\LightCacheInside;
use ZM\Store\MySQL\SqlPoolStorage;
use ZM\Store\Redis\ZMRedisPool;
use ZM\Store\WorkerCache;
use ZM\Store\ZMBuf;
use ZM\Utils\DataProvider;
use ZM\Utils\HttpUtil;
@@ -70,6 +79,13 @@ class ServerEventHandler
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
ZMUtil::stop();
});
if(Framework::$argv["daemon"]) {
$daemon_data = json_encode([
"pid" => \server()->master_pid,
"stdout" => ZMConfig::get("global")["swoole"]["log_file"]
],128|256);
file_put_contents(DataProvider::getWorkingDir()."/.daemon_pid", $daemon_data);
}
if (Framework::$argv["watch"]) {
if (extension_loaded('inotify')) {
Console::warning("Enabled File watcher, do not use in production.");
@@ -78,11 +94,11 @@ class ServerEventHandler
$this->addWatcher(DataProvider::getWorkingDir() . "/src", $fd);
Event::add($fd, function () use ($fd) {
$r = inotify_read($fd);
var_dump($r);
dump($r);
ZMUtil::reload();
});
} else {
Console::warning("You have not loaded inotify extension.");
Console::warning("You have not loaded \"inotify\" extension, please install first.");
}
}
}
@@ -114,7 +130,7 @@ class ServerEventHandler
Console::debug("正在关闭 " . ($server->taskworker ? "Task" : "") . "Worker 进程 " . Console::setColor("#" . \server()->worker_id, "gold") . TermColor::frontColor256(59) . ", pid=" . posix_getpid());
server()->stop($worker_id);
});
unset(Context::$context[Co::getCid()]);
unset(Context::$context[Coroutine::getCid()]);
if ($server->taskworker === false) {
try {
register_shutdown_function(function () use ($server) {
@@ -194,7 +210,7 @@ class ServerEventHandler
// 开箱即用的Redis
$redis = ZMConfig::get("global", "redis_config");
if($redis !== null && $redis["host"] != "") {
if ($redis !== null && $redis["host"] != "") {
if (!extension_loaded("redis")) Console::error("Can not find redis extension.\n");
else ZMRedisPool::init($redis);
}
@@ -211,7 +227,8 @@ class ServerEventHandler
return server()->worker_id === $v->worker_id || $v->worker_id === -1;
});
$dispatcher->dispatchEvents($server, $worker_id);
Console::debug("@OnStart 执行完毕");
if ($dispatcher->status === EventDispatcher::STATUS_NORMAL) Console::debug("@OnStart 执行完毕");
else Console::warning("@OnStart 执行异常!");
} catch (Exception $e) {
Console::error("Worker加载出错停止服务");
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
@@ -250,10 +267,17 @@ class ServerEventHandler
*/
public function onMessage($server, Frame $frame) {
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd.": ".TermColor::ITALIC.$frame->data.TermColor::RESET);
unset(Context::$context[Co::getCid()]);
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd . ": " . TermColor::ITALIC . $frame->data . TermColor::RESET);
unset(Context::$context[Coroutine::getCid()]);
$conn = ManagerGM::get($frame->fd);
set_coroutine_params(["server" => $server, "frame" => $frame, "connection" => $conn]);
$dispatcher1 = new EventDispatcher(OnMessageEvent::class);
$dispatcher1->setRuleFunction(function ($v) {
/** @noinspection PhpUnreachableStatementInspection */
return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";");
});
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
$dispatcher->setRuleFunction(function ($v) {
if ($v->getRule() == '') {
@@ -268,6 +292,7 @@ class ServerEventHandler
});
try {
//$starttime = microtime(true);
$dispatcher1->dispatchEvents($conn);
$dispatcher->dispatchEvents($conn);
//Console::success("Used ".round((microtime(true) - $starttime) * 1000, 3)." ms!");
} catch (Exception $e) {
@@ -287,12 +312,20 @@ class ServerEventHandler
* @param $request
* @param $response
*/
public function onRequest($request, $response) {
public function onRequest(?Request $request, ?\Swoole\Http\Response $response) {
$response = new Response($response);
foreach(ZMConfig::get("global")["http_header"] as $k => $v) {
$response->setHeader($k, $v);
}
unset(Context::$context[Co::getCid()]);
Console::debug("Calling Swoole \"request\" event from fd=" . $request->fd);
set_coroutine_params(["request" => $request, "response" => $response]);
$dis1 = new EventDispatcher(OnRequestEvent::class);
$dis1->setRuleFunction(function ($v) {
return eval("return " . $v->getRule() . ";") ? true : false;
});
$dis = new EventDispatcher(OnSwooleEvent::class);
$dis->setRuleFunction(function ($v) {
if ($v->getRule() == '') {
@@ -305,8 +338,9 @@ class ServerEventHandler
});
try {
$no_interrupt = $dis->dispatchEvents($request, $response);
if ($no_interrupt !== null) {
$dis1->dispatchEvents($request, $response);
$dis->dispatchEvents($request, $response);
if ($dis->status === EventDispatcher::STATUS_NORMAL && $dis1->status === EventDispatcher::STATUS_NORMAL) {
$result = HttpUtil::parseUri($request, $response, $request->server["request_uri"], $node, $params);
if ($result === true) {
ctx()->setCache("params", $params);
@@ -318,14 +352,16 @@ class ServerEventHandler
$div->request_method = $node["request_method"];
$div->class = $node["class"];
//Console::success("正在执行路由:".$node["method"]);
$r = $dispatcher->dispatchEvent($div, null, $params, $request, $response);
if (is_string($r) && !$response->isEnd()) $response->end($r);
$dispatcher->dispatchEvent($div, null, $params, $request, $response);
if (is_string($dispatcher->store) && !$response->isEnd()) $response->end($dispatcher->store);
}
}
if (!$response->isEnd()) {
//Console::warning('返回了404');
HttpUtil::responseCodePage($response, 404);
}
} catch (InterruptException $e) {
// do nothing
} catch (Exception $e) {
$response->status(500);
Console::info($request->server["remote_addr"] . ":" . $request->server["remote_port"] .
@@ -337,7 +373,7 @@ class ServerEventHandler
else
$response->end("Internal server error.");
}
Console::error("Internal server exception (500), caused by " . get_class($e).": ".$e->getMessage());
Console::error("Internal server exception (500), caused by " . get_class($e) . ": " . $e->getMessage());
Console::log($e->getTraceAsString(), "gray");
} catch (Error $e) {
$response->status(500);
@@ -351,7 +387,7 @@ class ServerEventHandler
else
$response->end("Internal server error.");
}
Console::error("Internal server error (500), caused by " . get_class($e).": ".$e->getMessage());
Console::error("Internal server error (500), caused by " . get_class($e) . ": " . $e->getMessage());
Console::log($e->getTraceAsString(), "gray");
}
}
@@ -364,12 +400,17 @@ class ServerEventHandler
public function onOpen($server, Request $request) {
Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd);
unset(Context::$context[Co::getCid()]);
$type = strtolower($request->get["type"] ?? $request->header["x-client-role"] ?? "");
$type = strtolower($request->header["x-client-role"] ?? $request->get["type"] ?? "");
$type_conn = ManagerGM::getTypeClassName($type);
ManagerGM::pushConnect($request->fd, $type_conn);
$conn = ManagerGM::get($request->fd);
set_coroutine_params(["server" => $server, "request" => $request, "connection" => $conn, "fd" => $request->fd]);
$conn->setOption("connect_id", strval($request->header["x-self-id"]) ?? "");
$conn->setOption("connect_id", strval($request->header["x-self-id"] ?? ""));
$dispatcher1 = new EventDispatcher(OnOpenEvent::class);
$dispatcher1->setRuleFunction(function ($v) {
return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";");
});
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
$dispatcher->setRuleFunction(function ($v) {
@@ -387,6 +428,7 @@ class ServerEventHandler
LightCacheInside::set("connect", "conn_fd", $request->fd);
}
}
$dispatcher1->dispatchEvents($conn);
$dispatcher->dispatchEvents($conn);
} catch (Exception $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
@@ -412,6 +454,10 @@ class ServerEventHandler
Console::debug("Calling Swoole \"close\" event from fd=" . $fd);
set_coroutine_params(["server" => $server, "connection" => $conn, "fd" => $fd]);
$dispatcher1 = new EventDispatcher(OnCloseEvent::class);
$dispatcher1->setRuleFunction(function ($v) {
return $v->connect_type == ctx()->getConnection()->getName() && eval("return " . $v->getRule() . ";");
});
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
$dispatcher->setRuleFunction(function ($v) {
@@ -429,6 +475,7 @@ class ServerEventHandler
LightCacheInside::set("connect", "conn_fd", -1);
}
}
$dispatcher1->dispatchEvents($conn);
$dispatcher->dispatchEvents($conn);
} catch (Exception $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
@@ -447,38 +494,81 @@ class ServerEventHandler
* @param $server
* @param $src_worker_id
* @param $data
* @throws InterruptException
*/
public function onPipeMessage(Server $server, $src_worker_id, $data) {
//var_dump($data, $server->worker_id);
//unset(Context::$context[Co::getCid()]);
$data = json_decode($data, true);
switch ($data["action"] ?? '') {
case "resume_ws_message":
$obj = $data["data"];
Co::resume($obj["coroutine"]);
case "getWorkerCache":
$r = WorkerCache::get($data["key"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case "stop":
Console::verbose('正在清理 #' . $server->worker_id . ' 的计时器');
Timer::clearAll();
case "setWorkerCache":
$r = WorkerCache::set($data["key"], $data["value"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case "terminate":
$server->stop();
case "unsetWorkerCache":
$r = WorkerCache::unset($data["key"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case 'echo':
Console::success('接收到来自 #' . $src_worker_id . ' 的消息');
case "asyncAddWorkerCache":
WorkerCache::add($data["key"], $data["value"], true);
break;
case 'send':
$server->sendMessage(json_encode(["action" => "echo"]), $data["target"]);
case "asyncSubWorkerCache":
WorkerCache::sub($data["key"], $data["value"], true);
break;
case "asyncSetWorkerCache":
WorkerCache::set($data["key"], $data["value"], true);
break;
case "asyncUnsetWorkerCache":
WorkerCache::unset($data["key"], true);
break;
case "addWorkerCache":
$r = WorkerCache::add($data["key"], $data["value"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case "subWorkerCache":
$r = WorkerCache::sub($data["key"], $data["value"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case "returnWorkerCache":
WorkerCache::$transfer[$data["cid"]] = $data["value"];
zm_resume($data["cid"]);
break;
default:
echo $data . PHP_EOL;
$dispatcher = new EventDispatcher(OnPipeMessageEvent::class);
$dispatcher->setRuleFunction(function (OnPipeMessageEvent $v) use ($data) {
return $v->action == $data["action"];
});
$dispatcher->dispatchEvents($data);
break;
}
}
/**
* @SwooleHandler("task")
* @param Server|null $server
* @param Server\Task $task
* @return mixed
*/
public function onTask() {
public function onTask(?Server $server, Server\Task $task) {
$data = $task->data;
switch($data["action"]) {
case "runMethod":
$c = $data["class"];
$ss = new $c();
$method = $data["method"];
$ps = $data["params"];
$task->finish($ss->$method(...$ps));
}
return null;
}
/**
@@ -505,7 +595,16 @@ class ServerEventHandler
//加载各个模块的注解类,以及反射
Console::debug("检索Module中");
$parser = new AnnotationParser();
$parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/Module/", "Module");
$path = DataProvider::getWorkingDir() . "/src/";
$dir = scandir($path);
unset($dir[0], $dir[1]);
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
foreach ($dir as $v) {
if (is_dir($path . "/" . $v) && isset($composer["autoload"]["psr-4"][$v . "\\"]) && !in_array($composer["autoload"]["psr-4"][$v . "\\"], $composer["extra"]["exclude_annotate"] ?? [])) {
Console::verbose("Add " . $v . " to register path");
$parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/" . $v . "/", $v);
}
}
$parser->registerMods();
EventManager::loadEventByParser($parser); //加载事件
@@ -518,14 +617,14 @@ class ServerEventHandler
//加载插件
$plugins = ZMConfig::get("global", "modules") ?? [];
if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false];
if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false, "message_level" => 99999];
if ($plugins["onebot"]) {
$obj = new OnSwooleEvent();
$obj->class = QQBot::class;
$obj->method = 'handle';
$obj->type = 'message';
$obj->level = 99999;
$obj->level = $plugins["onebot"]["message_level"] ?? 99999;
$obj->rule = 'connectIsQQ()';
EventManager::addEvent(OnSwooleEvent::class, $obj);
if ($plugins["onebot"]["single_bot_mode"]) {
@@ -536,7 +635,6 @@ class ServerEventHandler
}
//TODO: 编写加载外部插件的方式
$this->loadExternalModules($plugins);
}
private function addWatcher($maindir, $fd) {
@@ -550,11 +648,4 @@ class ServerEventHandler
}
}
}
private function loadExternalModules($plugins) {
foreach ($plugins as $k => $v) {
if ($k == "onebot") continue;
}
}
}

View File

@@ -54,6 +54,10 @@ class Framework
}
ZMAtomic::init();
try {
$sw = ZMConfig::get("global");
if (!is_dir($sw["zm_data"])) mkdir($sw["zm_data"]);
if (!is_dir($sw["config_dir"])) mkdir($sw["config_dir"]);
if (!is_dir($sw["crash_dir"])) mkdir($sw["crash_dir"]);
ManagerGM::init(ZMConfig::get("global", "swoole")["max_connection"] ?? 2048, 0.5, [
[
"key" => "connect_id",
@@ -90,6 +94,7 @@ class Framework
"version" => ZM_VERSION,
"config" => $args["env"] === null ? 'global.php' : $args["env"]
];
if(APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION;
if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) {
$out["task_worker_num"] = ZMConfig::get("global", "swoole")["task_worker_num"];
}
@@ -125,6 +130,40 @@ class Framework
LightCache::init($r);
LightCacheInside::init();
SpinLock::init($r["size"]);
set_error_handler(function ($error_no, $error_msg, $error_file, $error_line) {
switch ($error_no) {
case E_WARNING:
$level_tips = 'PHP Warning: ';
break;
case E_NOTICE:
$level_tips = 'PHP Notice: ';
break;
case E_DEPRECATED:
$level_tips = 'PHP Deprecated: ';
break;
case E_USER_ERROR:
$level_tips = 'User Error: ';
break;
case E_USER_WARNING:
$level_tips = 'User Warning: ';
break;
case E_USER_NOTICE:
$level_tips = 'User Notice: ';
break;
case E_USER_DEPRECATED:
$level_tips = 'User Deprecated: ';
break;
case E_STRICT:
$level_tips = 'PHP Strict: ';
break;
default:
$level_tips = 'Unkonw Type Error: ';
break;
} // do some handle
$error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line;
Console::warning($error); // 如果 return false 则错误会继续递交给 PHP 标准错误处理 /
return true;
}, E_ALL | E_STRICT);
} catch (Exception $e) {
Console::error("Framework初始化出现错误请检查");
Console::error($e->getMessage());
@@ -170,11 +209,9 @@ class Framework
}
}
foreach ($event_list as $k => $v) {
self::$server->on($k, function (...$param) use ($v) {
$c = ZMUtil::getModInstance($v->class);
$m = $v->method;
$c->$m(...$param);
});
$c = ZMUtil::getModInstance($v->class);
$m = $v->method;
self::$server->on($k, function (...$param) use ($c, $m) { $c->$m(...$param); });
}
}
@@ -213,11 +250,13 @@ class Framework
case 'daemon':
if ($y) {
$this->server_set["daemonize"] = 1;
Console::$theme = "no-color";
Console::log("已启用守护进程,输出重定向到 " . $this->server_set["log_file"]);
$terminal_id = null;
}
break;
case 'disable-console-input':
case 'no-interaction':
if ($y) $terminal_id = null;
break;
case 'log-error':
@@ -230,6 +269,7 @@ class Framework
if ($y) Console::setLevel(2);
break;
case 'log-verbose':
case 'verbose':
if ($y) Console::setLevel(3);
break;
case 'log-debug':
@@ -240,6 +280,10 @@ class Framework
Console::$theme = $y;
}
break;
default:
//Console::info("Calculating ".$x);
//dump($y);
break;
}
}
if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);

View File

@@ -94,7 +94,8 @@ class Response
*/
public function status($http_code, $reason = null) {
$this->status_code = $http_code;
return $this->response->status($http_code, $reason);
if (!$this->is_end) return $this->response->status($http_code, $reason);
else return false;
}
public function getStatusCode() {
@@ -107,7 +108,8 @@ class Response
* @return mixed
*/
public function setStatusCode($http_code, $reason = null) {
return $this->response->setStatusCode($http_code, $reason);
if (!$this->is_end) return $this->response->setStatusCode($http_code, $reason);
else return false;
}
/**
@@ -117,7 +119,8 @@ class Response
* @return mixed
*/
public function header($key, $value, $ucwords = null) {
return $this->response->header($key, $value, $ucwords);
if (!$this->is_end) return $this->response->header($key, $value, $ucwords);
else return false;
}
/**
@@ -127,7 +130,7 @@ class Response
* @return mixed
*/
public function setHeader($key, $value, $ucwords = null) {
return $this->response->setHeader($key, $value, $ucwords);
return !$this->is_end ? $this->response->setHeader($key, $value, $ucwords) : false;
}
/**
@@ -159,13 +162,17 @@ class Response
* @return mixed
*/
public function end($content = null) {
$this->is_end = true;
return $this->response->end($content);
if(!$this->is_end) {
$this->is_end = true;
return $this->response->end($content);
} else {
return false;
}
}
public function isEnd() { return $this->is_end; }
public function endWithStatus($status_code = 200, $content = null){
public function endWithStatus($status_code = 200, $content = null) {
$this->status($status_code);
$this->end($content);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace ZM\Http;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use ZM\Annotation\Http\Controller;
use ZM\Annotation\Http\RequestMapping;
use ZM\Console\Console;
class RouteManager
{
/** @var null|RouteCollection */
public static $routes = null;
public static function importRouteByAnnotation(RequestMapping $vss, $method, $class, $methods_annotations) {
if(self::$routes === null) self::$routes = new RouteCollection();
// 拿到所属方法的类上面有没有控制器的注解
$prefix = '';
foreach ($methods_annotations as $annotation) {
if ($annotation instanceof Controller) {
$prefix = $annotation->prefix;
break;
}
}
$tail = trim($vss->route, "/");
$route_name = $prefix.($tail === "" ? "" : "/").$tail;
Console::debug("添加路由:".$route_name);
$route = new Route($route_name, ['_class' => $class, '_method' => $method]);
$route->setMethods($vss->request_method);
self::$routes->add(md5($route_name), $route);
}
}

View File

@@ -35,7 +35,10 @@ class QQBot
set_coroutine_params(["data" => $data]);
ctx()->setCache("level", 0);
//Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd());
$this->dispatchBeforeEvents($data); // >= 200 的level before在这里执行
if ($data["post_type"] != "meta_event") {
$r = $this->dispatchBeforeEvents($data); // before在这里执行元事件不执行before为减少不必要的调试日志
if ($r->store === "block") EventDispatcher::interrupt();
}
if (CoMessage::resumeByWS()) {
EventDispatcher::interrupt();
}
@@ -49,24 +52,33 @@ class QQBot
}
}
/**
* @param $data
* @return EventDispatcher
* @throws InterruptException
*/
public function dispatchBeforeEvents($data) {
$before = new EventDispatcher(CQBefore::class);
$before->setRuleFunction(function ($v) use ($data) {
if ($v->level < 200) EventDispatcher::interrupt();
elseif ($v->cq_event != $data["post_type"]) return false;
return true;
return $v->cq_event == $data["post_type"];
});
$before->setReturnFunction(function ($result) {
if (!$result) EventDispatcher::interrupt();
if (!$result) EventDispatcher::interrupt("block");
});
$before->dispatchEvents($data);
return $before;
}
/**
* @param $data
* @throws InterruptException
*/
private function dispatchEvents($data) {
//Console::warning("最xia数据包".json_encode($data));
switch ($data["post_type"]) {
case "message":
$word = explodeMsg(str_replace("\r", "", context()->getMessage()));
if (empty($word)) $word = [""];
if (count(explode("\n", $word[0])) >= 2) {
$enter = explode("\n", context()->getMessage());
$first = split_explode(" ", array_shift($enter));
@@ -75,16 +87,15 @@ class QQBot
$word[$k] = trim($word[$k]);
}
}
//分发CQCommand事件
$dispatcher = new EventDispatcher(CQCommand::class);
$dispatcher->setRuleFunction(function (CQCommand $v) use ($word) {
if(array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) return false;
if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) return false;
elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == ctx()->getUserId())) &&
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (ctx()->getGroupId() ?? 0))) &&
($v->message_type == '' || ($v->message_type != '' && $v->message_type == ctx()->getMessageType()))
) {
if(($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) {
if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) {
array_shift($word);
ctx()->setCache("match", $word);
return true;
@@ -97,14 +108,14 @@ class QQBot
} elseif ($v->keyword != "" && mb_strpos(ctx()->getMessage(), $v->keyword) !== false) {
ctx()->setCache("match", explode($v->keyword, ctx()->getMessage()));
return true;
}elseif ($v->pattern != "") {
} elseif ($v->pattern != "") {
$match = matchArgs($v->pattern, ctx()->getMessage());
if($match !== false) {
if ($match !== false) {
ctx()->setCache("match", $match);
return true;
}
} elseif ($v->regex != "") {
if(preg_match("/" . $v->regex . "/u", ctx()->getMessage(), $word2) != 0) {
if (preg_match("/" . $v->regex . "/u", ctx()->getMessage(), $word2) != 0) {
ctx()->setCache("match", $word2);
return true;
}
@@ -114,10 +125,10 @@ class QQBot
});
$dispatcher->setReturnFunction(function ($result) {
if (is_string($result)) ctx()->reply($result);
EventDispatcher::interrupt();
if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt();
});
$r = $dispatcher->dispatchEvents();
if ($r === null) EventDispatcher::interrupt();
$dispatcher->dispatchEvents();
if ($dispatcher->status == EventDispatcher::STATUS_INTERRUPTED) EventDispatcher::interrupt();
//分发CQMessage事件
$msg_dispatcher = new EventDispatcher(CQMessage::class);
@@ -137,8 +148,7 @@ class QQBot
//Console::success("当前数据包:".json_encode(ctx()->getData()));
$dispatcher = new EventDispatcher(CQMetaEvent::class);
$dispatcher->setRuleFunction(function (CQMetaEvent $v) {
return ($v->meta_event_type == '' || ($v->meta_event_type != '' && $v->meta_event_type == ctx()->getData()["meta_event_type"])) &&
($v->sub_type == '' || ($v->sub_type != '' && $v->sub_type == (ctx()->getData()["sub_type"] ?? '')));
return ($v->meta_event_type == '' || ($v->meta_event_type != '' && $v->meta_event_type == ctx()->getData()["meta_event_type"]));
});
//eval(BP);
$dispatcher->dispatchEvents(ctx()->getData());

View File

@@ -7,6 +7,7 @@ namespace ZM\Store;
use Exception;
use Swoole\Table;
use ZM\Console\Console;
use ZM\Exception\ZMException;
class LightCache
{
@@ -19,6 +20,11 @@ class LightCache
public static $last_error = '';
/**
* @param $config
* @return bool|mixed
* @throws Exception
*/
public static function init($config) {
self::$config = $config;
self::$kv_table = new Table($config["size"], $config["hash_conflict_proportion"]);
@@ -50,11 +56,11 @@ class LightCache
/**
* @param string $key
* @return null|string
* @throws Exception
* @return null|mixed
* @throws ZMException
*/
public static function get(string $key) {
if (self::$kv_table === null) throw new Exception("not initialized LightCache");
if (self::$kv_table === null) throw new ZMException("not initialized LightCache");
self::checkExpire($key);
$r = self::$kv_table->get($key);
return $r === false ? null : self::parseGet($r);
@@ -63,10 +69,10 @@ class LightCache
/**
* @param string $key
* @return mixed|null
* @throws Exception
* @throws ZMException
*/
public static function getExpire(string $key) {
if (self::$kv_table === null) throw new Exception("not initialized LightCache");
if (self::$kv_table === null) throw new ZMException("not initialized LightCache");
self::checkExpire($key);
$r = self::$kv_table->get($key, "expire");
return $r === false ? null : $r - time();
@@ -77,10 +83,10 @@ class LightCache
* @param string|array|int $value
* @param int $expire
* @return mixed
* @throws Exception
* @throws ZMException
*/
public static function set(string $key, $value, int $expire = -1) {
if (self::$kv_table === null) throw new Exception("not initialized LightCache");
if (self::$kv_table === null) throw new ZMException("not initialized LightCache");
if (is_array($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
if (strlen($value) >= self::$config["max_strlen"]) return false;
@@ -93,7 +99,7 @@ class LightCache
$data_type = "bool";
$value = json_encode($value);
} else {
throw new Exception("Only can set string, array and int");
throw new ZMException("Only can set string, array and int");
}
try {
return self::$kv_table->set($key, [
@@ -110,10 +116,10 @@ class LightCache
* @param string $key
* @param $value
* @return bool|mixed
* @throws Exception
* @throws ZMException
*/
public static function update(string $key, $value) {
if (self::$kv_table === null) throw new Exception("not initialized LightCache.");
if (self::$kv_table === null) throw new ZMException("not initialized LightCache.");
if (is_array($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
if (strlen($value) >= self::$config["max_strlen"]) return false;
@@ -123,7 +129,7 @@ class LightCache
} elseif (is_int($value)) {
$data_type = "int";
} else {
throw new Exception("Only can set string, array and int");
throw new ZMException("Only can set string, array and int");
}
try {
if (self::$kv_table->get($key) === false) return false;

View File

@@ -0,0 +1,100 @@
<?php
namespace ZM\Store;
use ZM\Config\ZMConfig;
class WorkerCache
{
public static $config = null;
public static $store = [];
public static $transfer = [];
public static function get($key) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
if ($config["worker"] === server()->worker_id) {
return self::$store[$key] ?? null;
} else {
$action = ["action" => "getWorkerCache", "key" => $key, "cid" => zm_cid()];
server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
}
}
public static function set($key, $value, $async = false) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
if ($config["worker"] === server()->worker_id) {
self::$store[$key] = $value;
return true;
} else {
$action = ["action" => $async ? "asyncSetWorkerCache" : "setWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
if(!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
}
}
public static function unset($key, $async = false) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
if ($config["worker"] === server()->worker_id) {
unset(self::$store[$key]);
return true;
} else {
$action = ["action" => $async ? "asyncUnsetWorkerCache" : "unsetWorkerCache", "key" => $key, "cid" => zm_cid()];
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
if(!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
}
}
public static function add($key, int $value, $async = false) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
if ($config["worker"] === server()->worker_id) {
if(!isset(self::$store[$key])) self::$store[$key] = 0;
self::$store[$key] += $value;
return true;
} else {
$action = ["action" => $async ? "asyncAddWorkerCache" : "addWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
// if(!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
}
}
public static function sub($key, int $value, $async = false) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
if ($config["worker"] === server()->worker_id) {
if(!isset(self::$store[$key])) self::$store[$key] = 0;
self::$store[$key] -= $value;
return true;
} else {
$action = ["action" => $async ? "asyncSubWorkerCache" : "subWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
// if(!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
}
}
}

View File

@@ -48,34 +48,4 @@ class CoMessage
if ($result === null) return false;
return $result;
}
public static function resumeByWS() {
$dat = ctx()->getData();
$last = null;
SpinLock::lock("wait_api");
$all = LightCacheInside::get("wait_api", "wait_api") ?? [];
foreach ($all as $k => $v) {
if(!isset($v["compare"])) continue;
foreach ($v["compare"] as $vs) {
if ($v[$vs] != ($dat[$vs] ?? null)) {
continue 2;
}
}
$last = $k;
}
if($last !== null) {
$all[$last]["result"] = $dat;
LightCacheInside::set("wait_api", "wait_api", $all);
SpinLock::unlock("wait_api");
if ($all[$last]["worker_id"] != server()->worker_id) {
ZMUtil::sendActionToWorker($all[$k]["worker_id"], "resume_ws_message", $all[$last]);
} else {
Co::resume($all[$last]["coroutine"]);
}
return true;
} else {
SpinLock::unlock("wait_api");
return false;
}
}
}

View File

@@ -5,57 +5,47 @@ namespace ZM\Utils;
use Co;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Event\EventManager;
use ZM\Http\Response;
use ZM\Http\RouteManager;
class HttpUtil
{
public static function parseUri($request, $response, $uri, &$node, &$params) {
$uri = explode("/", $uri);
$uri = array_diff($uri, ["..", "", "."]);
$node = EventManager::$req_mapping;
$params = [];
while (true) {
$r = array_shift($uri);
if ($r === null) break;
if (($cnt = count($node["son"] ?? [])) == 1) {
if (isset($node["param_route"])) {
foreach ($node["son"] as $k => $v) {
if ($v["id"] == $node["param_route"]) {
$node = $v;
$params[mb_substr($v["name"], 1, -1)] = $r;
continue 2;
}
}
} elseif ($node["son"][0]["name"] == $r) {
$node = $node["son"][0];
continue;
}
} elseif ($cnt >= 1) {
if (isset($node["param_route"])) {
foreach ($node["son"] as $k => $v) {
if ($v["id"] == $node["param_route"]) {
$node = $v;
$params[mb_substr($v["name"], 1, -1)] = $r;
continue 2;
}
}
}
foreach ($node["son"] as $k => $v) {
if ($v["name"] == $r) {
$node = $v;
continue 2;
}
}
}
$context = new RequestContext();
$context->setMethod($request->server['request_method']);
try {
$matcher = new UrlMatcher(RouteManager::$routes ?? new RouteCollection(), $context);
$matched = $matcher->match($uri);
} catch (ResourceNotFoundException $e) {
if (ZMConfig::get("global", "static_file_server")["status"]) {
HttpUtil::handleStaticPage($request->server["request_uri"], $response);
return null;
}
$matched = null;
} catch (MethodNotAllowedException $e) {
$matched = null;
}
if ($matched !== null) {
$node = [
"route" => RouteManager::$routes->get($matched["_route"])->getPath(),
"class" => $matched["_class"],
"method" => $matched["_method"],
"request_method" => $request->server['request_method']
];
unset($matched["_class"], $matched["_method"]);
$params = $matched;
return true;
} else {
return false;
}
return !isset($node["route"]) ? false : true;
}
public static function getHttpCodePage(int $http_code) {
@@ -83,9 +73,9 @@ class HttpUtil
return true;
}
if (is_dir($path)) {
if(mb_substr($uri, -1, 1) != "/") {
if (mb_substr($uri, -1, 1) != "/") {
Console::info("[302] " . $uri);
$response->redirect($uri."/", 302);
$response->redirect($uri . "/", 302);
return true;
}
foreach ($base_index as $vp) {

View File

@@ -0,0 +1,17 @@
<?php
namespace ZM\Utils;
class ProcessManager
{
public static function runOnTask($param, $timeout = 0.5, $dst_worker_id = -1) {
return server()->taskwait([
"action" => "runMethod",
"class" => $param["class"],
"method" => $param["method"],
"params" => $param["params"]
], $timeout, $dst_worker_id);
}
}

View File

@@ -11,6 +11,8 @@ trait SingletonTrait
*/
private static $instance;
protected static $cached = [];
/**
* @return self
*/

View File

@@ -51,8 +51,4 @@ class ZMUtil
return ZMBuf::$instance[$class];
}
}
public static function sendActionToWorker($target_id, $action, $data) {
server()->sendMessage(json_encode(["action" => $action, "data" => $data]), $target_id);
}
}

View File

@@ -1,10 +1,12 @@
<?php
<?php #plain
use ZM\Config\ZMConfig;
use ZM\Utils\DataProvider;
define("ZM_START_TIME", microtime(true));
define("ZM_DATA", ZMConfig::get("global", "zm_data"));
define("ZM_VERSION", json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "unknown");
define("APP_VERSION", json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true)["version"] ?? "unknown");
define("CRASH_DIR", ZMConfig::get("global", "crash_dir"));
@mkdir(ZM_DATA);
@mkdir(CRASH_DIR);

View File

@@ -1,4 +1,4 @@
<?php
<?php #plain
use Swoole\Coroutine;
use ZM\API\ZMRobot;
@@ -88,6 +88,13 @@ function getAllClasses($dir, $indoor_name) {
//echo "At " . $indoor_name . PHP_EOL;
if (is_dir($dir . $v)) $classes = array_merge($classes, getAllClasses($dir . $v . "/", $indoor_name . "\\" . $v));
elseif (mb_substr($v, -4) == ".php") {
if(substr(file_get_contents($dir.$v), 6, 6) == "#plain") continue;
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir()."/composer.json"), true);
foreach($composer["autoload"]["files"] as $fi) {
if(realpath(DataProvider::getWorkingDir()."/".$fi) == realpath($dir.$v)) {
continue 2;
}
}
$class_name = $indoor_name . "\\" . mb_substr($v, 0, -4);
$classes [] = $class_name;
}
@@ -288,6 +295,16 @@ function bot() {
}
}
/**
* 获取同类型所有连接的文件描述符 ID
* @param string $type
* @return array
* @author 854854321
*/
function getAllFdByConnectType(string $type = 'default'): array {
$fds = [];
foreach (ManagerGM::getAllByName($type) as $obj) {
$fds[] = $obj->getFd();
}
return $fds;
}

View File

@@ -2,13 +2,17 @@
require __DIR__ . "/../vendor/autoload.php";
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$root = new RouteCollection();
$route = new Route('/foo', array('controller' => 'MyController'));
$routes = new RouteCollection();
$routes->add('route_name', $route);
@@ -35,30 +39,37 @@ $routes->add('date', new Route(
array() // methods
));
$route = new Route('/archive/test');
$routes->add('qwerty', $route);
$route = new Route('/');
$route = new Route('/{aas}/{test}', ['_class' => stdClass::class, '_method' => 'foo'],[],["class" => stdClass::class]);
$routes->add('root', $route);
$context = new RequestContext();
$matcher = new UrlMatcher($routes, $context);
//$root->addCollection($routes);
$matcher = new UrlMatcher($root, $context);
$root->addCollection($routes);
dump($root->all());
//$parameters = $matcher->match('/test/foo');var_dump($parameters);
$parameters = $matcher->match('/archive/2012-01');
var_dump($parameters);
// array(
// 'controller' => 'showArchive',
// 'month' => '2012-01',
// 'subdomain' => 'www',
// '_route' => ...
// )
try {
$parameters = $matcher->match('/fooss/%20');
var_dump($parameters);
} catch (ResourceNotFoundException $e) {
echo $e->getMessage().PHP_EOL;
} catch (MethodNotAllowedException $e) {
}
$parameters = $matcher->match('/');
var_dump($parameters);
$sub = new RouteCollection();

10
test/usage_test.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
use ZM\Exception\ZMException;
use ZM\Store\LightCache;
LightCache::getMemoryUsage();
try {
LightCache::getExpire('1');
} catch (ZMException $e) {
}