mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-07-02 22:35:38 +08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61e3818563 | ||
|
|
776ec98a3e | ||
|
|
f3e844bb0a | ||
|
|
a55cd4ed05 | ||
|
|
8a985620f9 | ||
|
|
484ddf9dfa | ||
|
|
b611b4aad6 | ||
|
|
b9f973c718 | ||
|
|
cd6c971547 | ||
|
|
c68083484a | ||
|
|
f999e689bf | ||
|
|
187a08a621 | ||
|
|
c208298937 | ||
|
|
e9e3e5e129 | ||
|
|
1ef8225d10 | ||
|
|
ccadec23e4 | ||
|
|
0972a1959e | ||
|
|
ce74191947 | ||
|
|
4feeb9519c | ||
|
|
efee146215 | ||
|
|
96ce7b30d0 | ||
|
|
076339baec | ||
|
|
50026be73d | ||
|
|
a1ad634926 | ||
|
|
0a5defaf29 | ||
|
|
557efc47a8 | ||
|
|
c566f940e0 | ||
|
|
381062c6c5 | ||
|
|
b31876025e | ||
|
|
ae8b0acdaa | ||
|
|
20ca3e7416 | ||
|
|
a1bfc031b8 | ||
|
|
8a6f8f54a5 | ||
|
|
19f0bffcd8 | ||
|
|
6337b626d6 | ||
|
|
7434bac94e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ composer.lock
|
||||
/bin/.phpunit.result.cache
|
||||
/resources/zhamao.service
|
||||
.phpunit.result.cache
|
||||
.daemon_pid
|
||||
@@ -60,7 +60,7 @@ public function index() {
|
||||
- 易用的上下文,模块内随处可用
|
||||
- 采用模块化编写,可单独拆装功能
|
||||
- 常驻内存,全局缓存变量随处使用
|
||||
- 自带 MySQL、Refis 等数据库连接池等数据库连接方案
|
||||
- 自带 MySQL、Redis 等数据库连接池等数据库连接方案
|
||||
- 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口
|
||||
- 静态文件服务器
|
||||
|
||||
@@ -84,6 +84,8 @@ public function index() {
|
||||
## 关于
|
||||
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
|
||||
|
||||
作者的炸毛机器人已从2018年初起稳定运行了**三年**,并且持续迭代。
|
||||
|
||||
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ(627577391)或提交 Issue 进行疑难解答。
|
||||
|
||||
本项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的模块代码做好备份。
|
||||
|
||||
22
bin/start
22
bin/start
@@ -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();
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
"description": "High performance QQ robot and web server development framework",
|
||||
"minimum-stability": "stable",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.0.2",
|
||||
"extra": [],
|
||||
"version": "2.2.2",
|
||||
"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": "*",
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
|
||||
143
docs/advanced/connect-ws-client.md
Normal file
143
docs/advanced/connect-ws-client.md
Normal 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());
|
||||
```
|
||||
|
||||
119
docs/advanced/custom-start.md
Normal file
119
docs/advanced/custom-start.md
Normal 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` 配置文件中的配置。
|
||||
6
docs/advanced/framework-structure.md
Normal file
6
docs/advanced/framework-structure.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 框架剖析
|
||||
|
||||
## 框架运行总结构图
|
||||
|
||||

|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
# 进阶开发
|
||||
## 深入
|
||||
还没填坑,敬请期待!
|
||||
在本章,下面的部分将详细说明一些具体的案例和自定义框架的操作。
|
||||
|
||||
- 如何自定义修改框架本身?- [框架启动方式](/advanced/custom-start/)
|
||||
- 如何接入一个自己的 WebSocket 客户端?- [接入 WebSocket 客户端](/advanced/connect-ws-client/)
|
||||
- 框架到底是怎么工作的?- [框架结构剖析](/advanced/framework-structure/)
|
||||
|
||||
> 更多进阶教程敬请期待....(或者你可以选择提 Issue 到框架 GitHub,有需求就写入文档)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Swoole\Http\Request
|
||||
|
||||
此类是 Swoole 内部的一个类,一般在收到 HTTP 请求时,在 `@RequestMapping` 或 `@OnSwooleEvent("request")` 两个注解下可用,用作获取 GET、POST参数,上传到后端的文件、Cookies 等。详见 [Swoole 文档 - Request](http://wiki.swoole.com/#/http_server?id=httprequest) 。
|
||||
此类是 Swoole 内部的一个类,一般在收到 HTTP 请求时,在 `@RequestMapping` 或 `@OnRequestEvent()` 两个注解下可用,用作获取 GET、POST参数,上传到后端的文件、Cookies 等。详见 [Swoole 文档 - Request](http://wiki.swoole.com/#/http_server?id=httprequest) 。
|
||||
|
||||
### 属性
|
||||
|
||||
|
||||
BIN
docs/assets/img/Untitled Diagram.png
Normal file
BIN
docs/assets/img/Untitled Diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/assets/img/diagram3.dbb4e32e.png
Normal file
BIN
docs/assets/img/diagram3.dbb4e32e.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/assets/img/diagram4.16ce39ca.png
Normal file
BIN
docs/assets/img/diagram4.16ce39ca.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/assets/img/framework-structure.png
Normal file
BIN
docs/assets/img/framework-structure.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
65
docs/component/atomics.md
Normal file
65
docs/component/atomics.md
Normal 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
177
docs/component/console.md
Normal 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 带有任何颜色,使用无色主题
|
||||
```
|
||||
|
||||
@@ -26,19 +26,19 @@ public function hello() {
|
||||
|
||||
获取 Swoole WebSocker Server 对象。此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server)。
|
||||
|
||||
可以使用的事件:`@OnSwooleEvent("message")`,`@OnSwooleEvent("open")`,`@OnSwooleEvent("close")`,`@OnStart()` 以及所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等。
|
||||
可以使用的事件:`@OnMessageEvent()`,`@OnOpenEvent()`,`@OnCloseEvent()`,`@OnStart()` 以及所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等。
|
||||
|
||||
## getFrame() - 获取 WS 数据帧
|
||||
|
||||
获取 `\Swoole\Websocket\Frame` 对象,此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)。
|
||||
|
||||
可以使用的事件:`@OnSwooleEvent("message")` 以及所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等,
|
||||
可以使用的事件:`@OnMessageEvent()` 以及所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等,
|
||||
|
||||
## getFd() - 返回 fd 值
|
||||
|
||||
获取当前连入 Swoole 服务器的连接文件描述符 ID。返回 int。一般代表连接号,可用来绑定对应链接。
|
||||
|
||||
可以使用的事件:所有 **getFrame()** 可以使用的,`@OnSwooleEvent("open")`,`@OnSwooleEvent("close")`
|
||||
可以使用的事件:所有 **getFrame()** 可以使用的,`@OnOpenEvent()`,`@OnCloseEvent()`
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
@@ -92,13 +92,13 @@ public function onMessage() {
|
||||
|
||||
返回 `\Swoole\Http\Request` 对象,可在 `@RequestMapping` 中使用,获取 Cookie,请求头,GET 参数什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httprequest)。
|
||||
|
||||
可以使用的事件:`@RequestMapping()`,`@OnSwooleEvent("request")`,`@OnSwooleEvent("open")`。
|
||||
可以使用的事件:`@RequestMapping()`,`@OnRequestEvent()`,`@OnOpenEvent()`。
|
||||
|
||||
## getResponse() - HTTP 响应对象
|
||||
|
||||
返回 `\Swoole\Http\Response` 对象的增强版,可在 HTTP 请求相关的事件中使用,返回内容和设置 Cookie 什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httpresponse)。
|
||||
|
||||
可以使用的事件:`@RequestMapping()`,`@OnSwooleEvent("request")`。
|
||||
可以使用的事件:`@RequestMapping()`,`@OnRequestEvent()`。
|
||||
|
||||
下面是使用以上两个功能的组合示例:
|
||||
|
||||
@@ -114,7 +114,7 @@ public function ping() {
|
||||
|
||||
## getConnection() - WS 连接对象
|
||||
|
||||
返回此上下文相关联的 WebSocket 连接对象。
|
||||
返回此上下文相关联的 WebSocket 连接对象。详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client)。
|
||||
|
||||
可以使用的事件:所有 **getFrame()** 可以使用的都可以使用。
|
||||
|
||||
@@ -124,7 +124,7 @@ public function ping() {
|
||||
|
||||
## getRobot() - 获取机器人 API 对象
|
||||
|
||||
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](机器人API.md)。
|
||||
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](robot-api.md)。
|
||||
|
||||
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等。
|
||||
|
||||
@@ -254,7 +254,7 @@ if($r["retcode"] == 0) Console::success("消息发送成功!");
|
||||
|
||||
参数同 `reply()`。
|
||||
|
||||
## waitMessage()
|
||||
## waitMessage() - 等待用户消息
|
||||
|
||||
- 参数:`waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "")`
|
||||
- 用途:等待用户输入消息
|
||||
@@ -286,14 +286,139 @@ function yourName(){
|
||||
( 你都10分钟不理我了,嘤嘤嘤
|
||||
</chat-box>
|
||||
|
||||
## getArgs()
|
||||
## getArgs() - 自动获取参数
|
||||
|
||||
TODO:还没写到这里,下次更新,今晚太困了。
|
||||
为 `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()
|
||||
|
||||
t
|
||||
获取整个上下文的所有内容的数组形式。
|
||||
|
||||
```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>
|
||||
63
docs/component/coroutine-pool.md
Normal file
63
docs/component/coroutine-pool.md
Normal 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 配置文件中指定的最大协程数量。
|
||||
|
||||
@@ -76,6 +76,41 @@ class Hello {
|
||||
[ https://zhamao.xin/file/hello.jpg
|
||||
</chat-box>
|
||||
|
||||
## CQ 码操作
|
||||
|
||||
### CQ::decode()
|
||||
|
||||
CQ 码字符反转义。
|
||||
|
||||
| 反转义前 | 反转义后 |
|
||||
| -------- | -------- |
|
||||
| `&` | `&` |
|
||||
| `[` | `[` |
|
||||
| `]` | `]` |
|
||||
|
||||
```php
|
||||
$str = CQ::decode("[我只是一条普通的文本]");
|
||||
// 转换为 "[我只是一条普通的文本]"
|
||||
```
|
||||
|
||||
### CQ::encode()
|
||||
|
||||
转义 CQ 码的敏感符号,防止 酷Q 把不该解析为 CQ 码的消息内容当作 CQ 码处理。
|
||||
|
||||
```php
|
||||
$str = CQ::encode("[CQ:我只是一条普通的文本]");
|
||||
// $str: "[CQ:我只是一条普通的文本]"
|
||||
```
|
||||
|
||||
### CQ::removeCQ()
|
||||
|
||||
去除字符串中所有的 CQ 码。
|
||||
|
||||
```php
|
||||
$str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]");
|
||||
// $str: "这是带表情的全体消息"
|
||||
```
|
||||
|
||||
## CQ 码列表
|
||||
|
||||
### CQ::face() - 发送 QQ 表情
|
||||
231
docs/component/global-functions.md
Normal file
231
docs/component/global-functions.md
Normal 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, "你好啊!!");
|
||||
```
|
||||
|
||||
|
||||
|
||||
320
docs/component/light-cache.md
Normal file
320
docs/component/light-cache.md
Normal 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
97
docs/component/mysql.md
Normal 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
62
docs/component/redis.md
Normal 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()` 方法里面的时候,后面的代码不会提前执行,是顺序执行的。回调的作用仅仅是用作自动回收连接对象。
|
||||
43
docs/component/singleton-trait.md
Normal file
43
docs/component/singleton-trait.md
Normal 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;` 一句话即可。
|
||||
73
docs/component/spin-lock.md
Normal file
73
docs/component/spin-lock.md
Normal 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
272
docs/component/zmrequest.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# ZMRequest(HTTP 客户端)
|
||||
|
||||
框架提供了轻量的 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
23
docs/component/zmutil.md
Normal 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;
|
||||
```
|
||||
|
||||
387
docs/event/framework-annotations.md
Normal file
387
docs/event/framework-annotations.md
Normal 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 轻量缓存
|
||||
}
|
||||
```
|
||||
|
||||
167
docs/event/middleware.md
Normal file
167
docs/event/middleware.md
Normal 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` 对象。
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 中间件注解
|
||||
|
||||
TODO:师傅,莫催,快肝完了!
|
||||
@@ -1,72 +0,0 @@
|
||||
# 框架核心注解事件
|
||||
|
||||
框架核心注解事件区别于机器人和路由注解事件,这里框架注解事件都是**直接**或封装调用 Swoole 的回调事件的,所以对一些比较底层或者基础的操作都在这里做,例如收到 HTTP 或 WebSocket 连接后执行的事件函数。
|
||||
|
||||
## OnSwooleEvent()
|
||||
|
||||
绑定 Swoole 所相关的事件,例如 WebSocket 接入、收到 WS 消息、关闭 WS 连接,HTTP 请求到达等。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ------------ | ------------------------------------------ |
|
||||
| 名称 | `@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/inside-class/) 类型,返回一个当前 WS 连接的连接对象。
|
||||
|
||||
### 示例1(机器人连接框架后输出信息)
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
use ZM\Annotation\Swoole\OnSwooleEvent;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Console\Console;
|
||||
class Hello {
|
||||
/**
|
||||
* 在机器人客户端连接框架后向终端输出信息
|
||||
* @OnSwooleEvent("open",rule="connectIsQQ()")
|
||||
* @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 导致的多条请求并发和干扰
|
||||
* @OnSwooleEvent("request",rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
|
||||
*/
|
||||
public function onRequest() {
|
||||
EventDispatcher::interrupt();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中 EventDispatcher 为事件分发器,interrupt 是通用阻断方法,如果你平常只使用阻断,则只需掌握这一个方法即可,`EventDispatcher::interrupt()` 在所有事件内可用。
|
||||
@@ -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)来编写配置文件。
|
||||
@@ -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
|
||||
10
docs/guide/quickstart-http.md
Normal file
10
docs/guide/quickstart-http.md
Normal 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/)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# 快速上手 - HTTP 服务器篇
|
||||
|
||||
HTTP 服务器篇暂时先放一放,大家应该主要都是奔着机器人开发来的吧~
|
||||
|
||||
@@ -33,7 +33,7 @@ public function index() {
|
||||
|
||||
首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
|
||||
|
||||
1. Linux 命令行基础
|
||||
1. Linux 命令行(会跑 Linux 程序)
|
||||
2. php 7.2+ 开发环境
|
||||
3. HTTP 协议(可选)
|
||||
4. OneBot 机器人聊天接口标准(可选)
|
||||
|
||||
@@ -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,13 @@ 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>';
|
||||
|
||||
@@ -1,8 +1,101 @@
|
||||
# 更新日志(v2 版本)
|
||||
|
||||
## v2.2.2
|
||||
|
||||
> 更新时间:2021.1.29
|
||||
|
||||
- 修复:模块文件错误时避免循环报错
|
||||
- 优化:代码结构
|
||||
- 修复:在不同进程时调用机器人 API 无法返回且报错的 bug
|
||||
- **修复:机器人无法连接的问题(2.1.6 ~ 2.2.1 受影响)**
|
||||
|
||||
## v2.2.1
|
||||
|
||||
> 更新时间:2021.1.29
|
||||
|
||||
- 修复:配置文件兼容性问题
|
||||
|
||||
## 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
|
||||
> 更新时间:2020.12.23
|
||||
|
||||
- 修复:开屏报错文件夹不存在
|
||||
|
||||
|
||||
51
mkdocs.yml
51
mkdocs.yml
@@ -9,6 +9,9 @@ theme:
|
||||
logo: assets/logos.png
|
||||
favicon: assets/favicon.png
|
||||
language: zh
|
||||
palette:
|
||||
primary: blue
|
||||
accent: blue
|
||||
features:
|
||||
- navigation.tabs
|
||||
extra_javascript:
|
||||
@@ -52,30 +55,46 @@ copyright: 'Copyright © 2019 - 2020 CrazyBot Team &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/机器人注解事件.md
|
||||
- HTTP 路由注解事件: event/路由注解事件.md
|
||||
- 框架核心注解事件: event/框架注解事件.md
|
||||
- 中间件注解: event/中间件.md
|
||||
- 自定义注解: event/自定义注解.md
|
||||
- 事件分发器: event/事件分发器.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/机器人API.md
|
||||
- CQ 码(多媒体消息): component/CQ码.md
|
||||
- 上下文: component/上下文.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
|
||||
- 内部类文件手册: advanced/inside-class.md
|
||||
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
|
||||
- 框架多进程: advanced/multi-process.md
|
||||
- FAQ: FAQ.md
|
||||
- 更新日志:
|
||||
- 更新日志(v2): update/v2.md
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Custom\Command;
|
||||
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class CustomCommand extends Command
|
||||
{
|
||||
// the name of the command (the part after "bin/console")
|
||||
protected static $defaultName = 'custom';
|
||||
|
||||
protected function configure() {
|
||||
$this->setDescription("custom description | 自定义命令的描述字段");
|
||||
$this->addOption("failure", null, null, "以错误码为1返回结果");
|
||||
// ...
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
if ($input->getOption("failure")) {
|
||||
$output->writeln("<error>Hello error! I am wrong message.</error>");
|
||||
return Command::FAILURE;
|
||||
} else {
|
||||
$output->writeln("<comment>Hello world! I am successful message.</comment>");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpFullyQualifiedNameUsageInspection */ #plain
|
||||
|
||||
//这里写你的全局函数
|
||||
function pgo(callable $func, $name = "default") {
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,16 @@
|
||||
|
||||
namespace ZM\API;
|
||||
|
||||
use Co;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
use ZM\Store\ZMAtomic;
|
||||
use ZM\Utils\CoMessage;
|
||||
|
||||
trait CQAPI
|
||||
{
|
||||
/**
|
||||
* @param ConnectionObject $connection
|
||||
* @param $connection
|
||||
* @param $reply
|
||||
* @param |null $function
|
||||
* @return bool|array
|
||||
@@ -35,21 +34,20 @@ trait CQAPI
|
||||
$r[$api_id] = [
|
||||
"data" => $reply,
|
||||
"time" => microtime(true),
|
||||
"self_id" => $connection->getOption("connect_id")
|
||||
"self_id" => $connection->getOption("connect_id"),
|
||||
"echo" => $api_id
|
||||
];
|
||||
if ($function === true) $r[$api_id]["coroutine"] = Co::getuid();
|
||||
LightCacheInside::set("wait_api", "wait_api", $r);
|
||||
SpinLock::unlock("wait_api");
|
||||
if (server()->push($connection->getFd(), json_encode($reply))) {
|
||||
if ($function === true) {
|
||||
Co::suspend();
|
||||
return CoMessage::yieldByWS($r[$api_id], ["echo"], 60);
|
||||
} else {
|
||||
SpinLock::lock("wait_api");
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
$data = $r[$api_id];
|
||||
unset($r[$api_id]);
|
||||
LightCacheInside::set("wait_api", "wait_api", $r);
|
||||
SpinLock::unlock("wait_api");
|
||||
return isset($data['result']) ? $data['result'] : null;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -228,9 +228,9 @@ class ZMRobot
|
||||
/**
|
||||
* 群组单人禁言
|
||||
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
|
||||
* @param int $group_id
|
||||
* @param int $user_id
|
||||
* @param int $duration
|
||||
* @param $group_id
|
||||
* @param $user_id
|
||||
* @param $duration
|
||||
* @return array|bool|null
|
||||
*/
|
||||
public function setGroupBan($group_id, $user_id, $duration = 1800) {
|
||||
|
||||
@@ -9,10 +9,10 @@ use ZM\Console\Console;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
use ZM\Annotation\Http\{HandleAfter, HandleBefore, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping};
|
||||
use ZM\Annotation\Http\{HandleAfter, HandleBefore, HandleException, Middleware, MiddlewareClass, RequestMapping};
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
use ZM\Annotation\Module\Closed;
|
||||
use ZM\Utils\DataProvider;
|
||||
use ZM\Http\RouteManager;
|
||||
|
||||
class AnnotationParser
|
||||
{
|
||||
@@ -33,7 +33,7 @@ class AnnotationParser
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->start_time = microtime(true);
|
||||
$this->loadAnnotationClasses();
|
||||
//$this->loadAnnotationClasses();
|
||||
$this->req_mapping[0] = [
|
||||
'id' => 0,
|
||||
'pid' => -1,
|
||||
@@ -88,8 +88,9 @@ 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;
|
||||
/** @noinspection PhpUndefinedFieldInspection */
|
||||
$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) {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
namespace ZM\Annotation\CQ;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Required;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
namespace ZM\Annotation\Http;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Required;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
|
||||
|
||||
21
src/ZM/Annotation/Swoole/OnCloseEvent.php
Normal file
21
src/ZM/Annotation/Swoole/OnCloseEvent.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZM\Annotation\Swoole;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
* Class OnCloseEvent
|
||||
* @package ZM\Annotation\Swoole
|
||||
*/
|
||||
class OnCloseEvent extends OnSwooleEventBase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $connect_type = "default";
|
||||
}
|
||||
21
src/ZM/Annotation/Swoole/OnMessageEvent.php
Normal file
21
src/ZM/Annotation/Swoole/OnMessageEvent.php
Normal 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";
|
||||
}
|
||||
21
src/ZM/Annotation/Swoole/OnOpenEvent.php
Normal file
21
src/ZM/Annotation/Swoole/OnOpenEvent.php
Normal 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";
|
||||
}
|
||||
24
src/ZM/Annotation/Swoole/OnPipeMessageEvent.php
Normal file
24
src/ZM/Annotation/Swoole/OnPipeMessageEvent.php
Normal 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;
|
||||
}
|
||||
17
src/ZM/Annotation/Swoole/OnRequestEvent.php
Normal file
17
src/ZM/Annotation/Swoole/OnRequestEvent.php
Normal 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
|
||||
{
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use ZM\Annotation\AnnotationBase;
|
||||
* Class OnWorkerStart
|
||||
* @package ZM\Annotation\Swoole
|
||||
* @Annotation
|
||||
* @Target("ALL")
|
||||
* @Target("METHOD")
|
||||
*/
|
||||
class OnStart extends AnnotationBase
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
49
src/ZM/Annotation/Swoole/OnSwooleEventBase.php
Normal file
49
src/ZM/Annotation/Swoole/OnSwooleEventBase.php
Normal 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;
|
||||
}
|
||||
}
|
||||
31
src/ZM/Command/DaemonCommand.php
Normal file
31
src/ZM/Command/DaemonCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
24
src/ZM/Command/DaemonReloadCommand.php
Normal file
24
src/ZM/Command/DaemonReloadCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
30
src/ZM/Command/DaemonStatusCommand.php
Normal file
30
src/ZM/Command/DaemonStatusCommand.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZM\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
26
src/ZM/Command/DaemonStopCommand.php
Normal file
26
src/ZM/Command/DaemonStopCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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__);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -145,7 +145,7 @@ class Context implements ContextInterface
|
||||
if ($prompt != "") $this->reply($prompt);
|
||||
|
||||
try {
|
||||
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())]);
|
||||
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())], $timeout);
|
||||
} catch (Exception $e) {
|
||||
$r = false;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -117,6 +117,8 @@ interface ContextInterface
|
||||
|
||||
public function cloneFromParent();
|
||||
|
||||
public function getNumArg($prompt_msg = "");
|
||||
|
||||
public function copy();
|
||||
|
||||
public function getOption();
|
||||
|
||||
@@ -31,7 +31,6 @@ class DB
|
||||
|
||||
/**
|
||||
* @param $table_name
|
||||
* @param bool $enable_cache
|
||||
* @return Table
|
||||
* @throws DbException
|
||||
*/
|
||||
@@ -95,6 +94,7 @@ class DB
|
||||
$ps = $conn->prepare($line);
|
||||
if ($ps === false) {
|
||||
SqlPoolStorage::$sql_pool->put(null);
|
||||
/** @noinspection PhpUndefinedFieldInspection */
|
||||
throw new DbException("SQL语句查询错误," . $line . ",错误信息:" . $conn->error);
|
||||
} else {
|
||||
if (!($ps instanceof PDOStatement) && !($ps instanceof PDOStatementProxy)) {
|
||||
|
||||
@@ -47,7 +47,7 @@ class Table
|
||||
return new DeleteBody($this);
|
||||
}
|
||||
|
||||
public function statement($line){
|
||||
public function statement(){
|
||||
$this->cache = [];
|
||||
//TODO: 无返回的statement语句
|
||||
}
|
||||
|
||||
@@ -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,49 @@ class EventDispatcher
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed ...$params
|
||||
* @throws Exception
|
||||
*/
|
||||
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 | 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 +156,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 +184,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -65,11 +74,18 @@ class ServerEventHandler
|
||||
}
|
||||
Process::signal(SIGINT, function () use ($r) {
|
||||
echo "\r";
|
||||
Console::warning("Server interrupted by keyboard on Master.");
|
||||
Console::warning("Server interrupted(SIGINT) on Master.");
|
||||
if ((Framework::$server->inotify ?? null) !== null)
|
||||
/** @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());
|
||||
@@ -221,7 +238,7 @@ class ServerEventHandler
|
||||
Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
|
||||
Console::error("Maybe it caused by your own code if in your own Module directory.");
|
||||
Console::log($e->getTraceAsString(), 'gray');
|
||||
ZMUtil::stop();
|
||||
posix_kill($server->master_pid, SIGINT);
|
||||
}
|
||||
} else {
|
||||
// 这里是TaskWorker初始化的内容部分
|
||||
@@ -238,7 +255,7 @@ class ServerEventHandler
|
||||
Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
|
||||
Console::error("Maybe it caused by your own code if in your own Module directory.");
|
||||
Console::log($e->getTraceAsString(), 'gray');
|
||||
ZMUtil::stop();
|
||||
posix_kill($server->master_pid, SIGINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,13 +400,18 @@ 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"] ?? ""));
|
||||
|
||||
$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) {
|
||||
if ($v->getRule() == '') {
|
||||
@@ -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() . ")";
|
||||
@@ -444,9 +491,10 @@ class ServerEventHandler
|
||||
|
||||
/**
|
||||
* @SwooleHandler("pipeMessage")
|
||||
* @param $server
|
||||
* @param Server $server
|
||||
* @param $src_worker_id
|
||||
* @param $data
|
||||
* @throws Exception
|
||||
*/
|
||||
public function onPipeMessage(Server $server, $src_worker_id, $data) {
|
||||
//var_dump($data, $server->worker_id);
|
||||
@@ -457,28 +505,75 @@ class ServerEventHandler
|
||||
$obj = $data["data"];
|
||||
Co::resume($obj["coroutine"]);
|
||||
break;
|
||||
case "stop":
|
||||
Console::verbose('正在清理 #' . $server->worker_id . ' 的计时器');
|
||||
Timer::clearAll();
|
||||
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 "terminate":
|
||||
$server->stop();
|
||||
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 'echo':
|
||||
Console::success('接收到来自 #' . $src_worker_id . ' 的消息');
|
||||
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 'send':
|
||||
$server->sendMessage(json_encode(["action" => "echo"]), $data["target"]);
|
||||
case "asyncAddWorkerCache":
|
||||
WorkerCache::add($data["key"], $data["value"], true);
|
||||
break;
|
||||
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
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
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 +600,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 +622,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 +640,6 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
//TODO: 编写加载外部插件的方式
|
||||
$this->loadExternalModules($plugins);
|
||||
}
|
||||
|
||||
private function addWatcher($maindir, $fd) {
|
||||
@@ -550,11 +653,4 @@ class ServerEventHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function loadExternalModules($plugins) {
|
||||
foreach ($plugins as $k => $v) {
|
||||
if ($k == "onebot") continue;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ 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"]);
|
||||
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",
|
||||
@@ -94,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"];
|
||||
}
|
||||
@@ -129,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());
|
||||
@@ -174,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); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,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':
|
||||
@@ -234,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':
|
||||
@@ -244,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);
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
namespace ZM\Http;
|
||||
|
||||
|
||||
use ZM\Console\Console;
|
||||
|
||||
class Response
|
||||
{
|
||||
|
||||
@@ -94,7 +92,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 +106,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 +117,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 +128,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 +160,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);
|
||||
}
|
||||
|
||||
37
src/ZM/Http/RouteManager.php
Normal file
37
src/ZM/Http/RouteManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace ZM\Module;
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use Exception;
|
||||
use ZM\Annotation\CQ\CQAPIResponse;
|
||||
use ZM\Annotation\CQ\CQBefore;
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
@@ -14,8 +14,6 @@ use ZM\Annotation\CQ\CQRequest;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Exception\InterruptException;
|
||||
use ZM\Exception\WaitTimeoutException;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
use ZM\Utils\CoMessage;
|
||||
|
||||
/**
|
||||
@@ -26,19 +24,25 @@ class QQBot
|
||||
{
|
||||
/**
|
||||
* @throws InterruptException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle() {
|
||||
try {
|
||||
$data = json_decode(context()->getFrame()->data, true);
|
||||
if (isset($data["post_type"])) {
|
||||
//echo TermColor::ITALIC.json_encode($data, 128|256).TermColor::RESET.PHP_EOL;
|
||||
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在这里执行
|
||||
set_coroutine_params(["data" => $data]);
|
||||
if (isset($data["echo"])) {
|
||||
if (CoMessage::resumeByWS()) {
|
||||
EventDispatcher::interrupt();
|
||||
}
|
||||
}
|
||||
if (isset($data["post_type"])) {
|
||||
//echo TermColor::ITALIC.json_encode($data, 128|256).TermColor::RESET.PHP_EOL;
|
||||
ctx()->setCache("level", 0);
|
||||
//Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd());
|
||||
if ($data["post_type"] != "meta_event") {
|
||||
$r = $this->dispatchBeforeEvents($data); // before在这里执行,元事件不执行before为减少不必要的调试日志
|
||||
if ($r->store === "block") EventDispatcher::interrupt();
|
||||
}
|
||||
//Console::warning("最上数据包:".json_encode($data));
|
||||
$this->dispatchEvents($data);
|
||||
} else {
|
||||
@@ -49,24 +53,33 @@ class QQBot
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @return EventDispatcher
|
||||
* @throws Exception
|
||||
*/
|
||||
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 +88,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 +109,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 +126,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 +149,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());
|
||||
@@ -167,45 +178,16 @@ class QQBot
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $req
|
||||
* @throws Exception
|
||||
*/
|
||||
private function dispatchAPIResponse($req) {
|
||||
$status = $req["status"];
|
||||
$retcode = $req["retcode"];
|
||||
$data = $req["data"];
|
||||
if (isset($req["echo"]) && is_numeric($req["echo"])) {
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
if (isset($r[$req["echo"]])) {
|
||||
$origin = $r[$req["echo"]];
|
||||
$self_id = $origin["self_id"];
|
||||
$response = [
|
||||
"status" => $status,
|
||||
"retcode" => $retcode,
|
||||
"data" => $data,
|
||||
"self_id" => $self_id,
|
||||
"echo" => $req["echo"]
|
||||
];
|
||||
set_coroutine_params(["cq_response" => $response]);
|
||||
$dispatcher = new EventDispatcher(CQAPIResponse::class);
|
||||
$dispatcher->setRuleFunction(function (CQAPIResponse $response) {
|
||||
return $response->retcode == ctx()->getCQResponse()["retcode"];
|
||||
});
|
||||
$dispatcher->dispatchEvents($response);
|
||||
|
||||
$origin_ctx = ctx()->copy();
|
||||
set_coroutine_params($origin_ctx);
|
||||
if (($origin["coroutine"] ?? false) !== false) {
|
||||
SpinLock::lock("wait_api");
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
$r[$req["echo"]]["result"] = $response;
|
||||
LightCacheInside::set("wait_api", "wait_api", $r);
|
||||
SpinLock::unlock("wait_api");
|
||||
Coroutine::resume($origin['coroutine']);
|
||||
}
|
||||
SpinLock::lock("wait_api");
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
unset($r[$req["echo"]]);
|
||||
LightCacheInside::set("wait_api", "wait_api", $r);
|
||||
SpinLock::unlock("wait_api");
|
||||
}
|
||||
}
|
||||
set_coroutine_params(["cq_response" => $req]);
|
||||
$dispatcher = new EventDispatcher(CQAPIResponse::class);
|
||||
$dispatcher->setRuleFunction(function (CQAPIResponse $response) {
|
||||
return $response->retcode == ctx()->getCQResponse()["retcode"];
|
||||
});
|
||||
$dispatcher->dispatchEvents($req);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,9 +23,10 @@ class LightCacheInside
|
||||
$result = self::$kv_table["wait_api"]->create() && self::$kv_table["connect"]->create();
|
||||
if ($result === false) {
|
||||
self::$last_error = '系统内存不足,申请失败';
|
||||
return $result;
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
86
src/ZM/Store/WorkerCache.php
Normal file
86
src/ZM/Store/WorkerCache.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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") ?? ["worker" => 0];
|
||||
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()];
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
|
||||
private static function processRemote($action, $async, $config) {
|
||||
$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()];
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
|
||||
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()];
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
|
||||
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()];
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class CoMessage
|
||||
* @param array $hang
|
||||
* @param array $compare
|
||||
* @param int $timeout
|
||||
* @return bool
|
||||
* @return mixed
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function yieldByWS(array $hang, array $compare, $timeout = 600) {
|
||||
@@ -57,7 +57,8 @@ class CoMessage
|
||||
foreach ($all as $k => $v) {
|
||||
if(!isset($v["compare"])) continue;
|
||||
foreach ($v["compare"] as $vs) {
|
||||
if ($v[$vs] != ($dat[$vs] ?? null)) {
|
||||
if (!isset($v[$vs], $dat[$vs])) continue 2;
|
||||
if ($v[$vs] != $dat[$vs]) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
@@ -68,7 +69,7 @@ class CoMessage
|
||||
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]);
|
||||
ZMUtil::sendActionToWorker($all[$last]["worker_id"], "resume_ws_message", $all[$last]);
|
||||
} else {
|
||||
Co::resume($all[$last]["coroutine"]);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
17
src/ZM/Utils/ProcessManager.php
Normal file
17
src/ZM/Utils/ProcessManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ trait SingletonTrait
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
protected static $cached = [];
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php #plain
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use ZM\API\ZMRobot;
|
||||
@@ -24,6 +24,7 @@ function phar_classloader($p) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once $filepath;
|
||||
} catch (Exception $e) {
|
||||
echo "Error when finding class: " . $p . PHP_EOL;
|
||||
@@ -75,7 +76,7 @@ function unicode_decode($str) {
|
||||
/**
|
||||
* 获取模块文件夹下的每个类文件的类名称
|
||||
* @param $dir
|
||||
* @param string $indoor_name
|
||||
* @param $indoor_name
|
||||
* @return array
|
||||
*/
|
||||
function getAllClasses($dir, $indoor_name) {
|
||||
@@ -88,6 +89,14 @@ 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;
|
||||
}
|
||||
}
|
||||
if ($v == "global_function.php") continue;
|
||||
$class_name = $indoor_name . "\\" . mb_substr($v, 0, -4);
|
||||
$classes [] = $class_name;
|
||||
}
|
||||
|
||||
@@ -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
10
test/usage_test.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
use ZM\Exception\ZMException;
|
||||
use ZM\Store\LightCache;
|
||||
|
||||
LightCache::getMemoryUsage();
|
||||
try {
|
||||
LightCache::getExpire('1');
|
||||
} catch (ZMException $e) {
|
||||
}
|
||||
Reference in New Issue
Block a user