Compare commits

..

27 Commits
2.1.5 ... 2.2.5

Author SHA1 Message Date
jerry
9ace85e604 update to 2.2.5 version again
add transaction for SpinLock.php
add getAllCQ() for CQ.php
fix CQ bug
update docs
2021-02-20 16:57:19 +08:00
jerry
f677b0e132 update to 2.2.5 version
add saveToJson and loadFromJson function for DataProvider.php
fix @OnSave annotation not working
adjust swoole timer tick
add hasKey() for WorkerCache.php
2021-02-15 15:15:26 +08:00
jerry
f137f044d0 Merge remote-tracking branch 'origin/master' 2021-02-09 17:09:26 +08:00
jerry
77c12db31a reformat code 2021-02-09 17:09:09 +08:00
Whale
b670cb29fe Update README.md 2021-02-09 11:12:29 +08:00
Whale
95d7bb071d Update README.md 2021-02-09 10:54:59 +08:00
Whale
eadb4c1dee Update README.md 2021-02-09 10:54:05 +08:00
Whale
6672a6c852 Update README.md 2021-02-09 10:53:52 +08:00
Whale
094feddda4 Update README.md 2021-02-09 10:53:15 +08:00
Whale
f86eddb298 Update README.md 2021-02-09 10:48:05 +08:00
Whale
a93b4917cd Update README.md 2021-02-09 10:47:40 +08:00
jerry
0f9767aa16 update docs 2021-02-07 11:48:55 +08:00
jerry
0c9f246690 update to 2.2.4 version
update docs
fix broken ssh caused cpu overloading
fix WorkerCache bug when no global config
add global function zm_atomic
2021-02-07 11:46:42 +08:00
jerry
517d258d61 update to 2.2.3 version, I am tired
fix access_token not working
fix waitMessage() not working in v2.2.2
2021-01-30 00:06:42 +08:00
jerry
61e3818563 update to 2.2.2 version finally
clean redundant code
fix API reply in @OnTick for multi-process
fix loop error reporting
2021-01-29 23:34:34 +08:00
jerry
776ec98a3e fix waitMessage timeout bug 2021-01-29 22:32:29 +08:00
jerry
f3e844bb0a update to 2.2.2 version
fix QQBot error
clean code
2021-01-29 22:27:10 +08:00
jerry
a55cd4ed05 update docs 2021-01-29 21:37:02 +08:00
jerry
8a985620f9 update to 2.2.1 version
fix a compatibility bug
2021-01-29 21:36:14 +08:00
jerry
484ddf9dfa update Hello.php
update docs
2021-01-29 21:30:19 +08:00
jerry
b611b4aad6 add DaemonCommand for daemon players
adjust http_header available
2021-01-29 20:47:00 +08:00
jerry
b9f973c718 add unset for WorkerCache.php 2021-01-20 18:43:22 +08:00
jerry
cd6c971547 update Singleton 2021-01-20 16:45:50 +08:00
jerry
c68083484a update to 2.2.0 version
add OnPipeMessageEvent.php
add ProcessManager.php
add WorkerCache component
fix route bug
correct Exception to ZMException
2021-01-20 16:11:04 +08:00
jerry
f999e689bf update docs 2021-01-18 18:09:33 +08:00
jerry
187a08a621 update to 2.1.6 version
优化代码结构
增加更多提示语
修复处理空格消息时的报错
修复上下文的bug
2021-01-18 18:08:29 +08:00
Whale
c208298937 Update README.md 2021-01-14 18:32:54 +08:00
77 changed files with 2170 additions and 349 deletions

1
.gitignore vendored
View File

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

View File

@@ -6,21 +6,19 @@
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)]()
[![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
[![Latest Stable Version](http://img.shields.io/packagist/v/zhamao/framework.svg)](https://packagist.org/packages/zhamao/framework)
[![Banner](https://img.shields.io/badge/CQHTTP-v11-black)]()
[![Banner](https://img.shields.io/badge/OneBot-v11-black)]()
[![stupid counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/stupid.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid)
[![TODO counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
[![注解数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/AnnotationBase.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=AnnotationBase)
[![TODO 数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
</div>
## 开发者注意
**开发者 QQ 群670821194**
开发者 QQ 群:**670821194**
**当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!**
当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!
**2.0 版本如果有问题请第一时间加群反馈!**
有关 3.0 版本的最新情况,请看这里:[Issue #22](https://github.com/zhamao-robot/zhamao-framework/issues/22)
2.0 版本如果有问题请第一时间加群反馈!
## 简介
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
@@ -53,16 +51,15 @@ public function index() {
自行构建文档:`mkdocs build -d distribute`
## 特点
- 支持多账号
- 原生为多账号设计,支持多个机器人负载均衡
- 使用 Swoole 多工作进程机制和协程加持,尽可能简单的情况下提升了性能
- 灵活的注解事件绑定机制
- 支持下断点调试Psysh
- 易用的上下文,模块内随处可用
- 采用模块化编写,可单独拆装功能
- 常驻内存,全局缓存变量随处使用
- 自带 MySQL、Refis 等数据库连接池等数据库连接方案
- 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口
- 静态文件服务器
- 采用模块化编写,可自由搭配其他 composer 组件
- 常驻内存,全局缓存变量随处使用,提供多种缓存方案
- 自带 MySQL、Redis 等数据库连接池等数据库连接方案
- 本身为 HTTP 服务器、WebSocket 服务器,可以构建属于自己的 HTTP API 接口
- 静态文件服务器,可将前端合并到一起
## 从 v1 升级
炸毛框架 v2 相对 v1 版本改动了不少内容,其中包括框架底层机制、注解事件分发、调试、命名空间等变化,详情可查看上方文档。
@@ -70,11 +67,13 @@ public function index() {
如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。
## 贡献和捐赠
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。
目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
本项目为作者闲暇时间开发,如果觉得好用,不妨进行捐助~你的捐助会让我更加有动力完善插件,感谢你的支持!
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。[捐赠列表](https://github.com/zhamao-robot/thanks)
### 支付宝
![支付宝二维码](/resources/images/alipay_img.jpg)
@@ -94,4 +93,6 @@ public function index() {
**注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。
在贡献代码时,请保管好自己的全局配置文件中的敏感信息,请勿将带有个人信息的配置文件上传 GitHub 等网站。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)

View File

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

View File

@@ -3,7 +3,7 @@
"description": "High performance QQ robot and web server development framework",
"minimum-stability": "stable",
"license": "Apache-2.0",
"version": "2.1.5",
"version": "2.2.5",
"extra": {
"exclude_annotate": [
"src/ZM"
@@ -37,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": "*",
@@ -52,6 +53,7 @@
]
},
"require-dev": {
"swoole/ide-helper": "@dev"
"swoole/ide-helper": "@dev",
"phpunit/phpunit": "^9.5"
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,231 @@
# 编写管理员专属功能
众所周知,如果大家使用炸毛框架来开发聊天机器人的话,会比较方便。但是有些地方你一定会感觉还是欠缺了点,比如下面这样,你想编写一个只能由机器人管理员,也就是你自己,才能触发的功能:
```php
/**
* @CQCommand(match="禁言",message_type="group")
*/
public function banSomeone() {
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
$r2 = ctx()->getFullArg("请输入禁言的时间(秒)");
$cq = CQ::getCQ($r1);
if ($cq !== null) {
if ($cq["type"] != "at") return "请at或者输入正确的QQ号";
$r1 = $cq["params"]["qq"];
}
// 群内禁言用户
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2);
return "禁言成功!";
}
```
这时候,如果只是自己有绝对的权利,可以将自己的 QQ 号写死在注解 `@CQCommand` 中,并限定 `user_id`(假设我的 QQ 号码为 123456
```php
/**
* @CQCommand(match="禁言",message_type="group",user_id=123456)
*/
```
但是,随着时间的推移,你的机器人伙伴群可能越来越大,这个命令可能不止需要绝对的你来使用,你还要将机器人的部分权利下发给更多的伙伴,怎么办呢?注解里面只能写死的。
答案很简单这时候我们就需要用到框架提供的中间件Middleware。中间件说白了就是在事件执行前、后、过程中抛出的异常对其进行阻断和插入代码比如我们上方在触发禁言这个注解事件前首先要判断执行这个命令的是不是钦定的管理员。
## 第一步:定义中间件
首先,我们需要定义一个中间件。在框架默认提供的脚手架中,包含了一个叫 `TimerMiddleware.php` 的示例中间件,这个示例中间件的目的是非常简单的,就是判断这个注解事件运行了多长时间。假设你有一个机器人功能,这个功能下的代码需要执行很长时间,可以使用这一注解轻松将事件执行的时间打印到终端上。
关于中间件的有关说明,见 [中间件](/event/middleware)。
下面我们假设你已经阅读过中间件注解的文档了,我们着手编写一个判断指令执行者是否是指定的管理员 QQ 的中间件。为了省事和让大家方便地复现,我先在脚手架下的目录 `src/Module/Middleware/` 下新建 PHP 类文件 `AdminMiddleware.php`(和 `TimerMiddleware.php` 在同一个目录)。
```php
<?php
namespace Module\Middleware;
use ZM\Annotation\Http\HandleBefore;
use ZM\Annotation\Http\MiddlewareClass;
use ZM\Exception\ZMException;
use ZM\Http\MiddlewareInterface;
use ZM\Store\LightCache;
/**
* Class AdminMiddleware
* 示例中间件:用于动态管理一些管理员指令的中间件
* @package Module\Middleware
* @MiddlewareClass("admin")
*/
class AdminMiddleware implements MiddlewareInterface
{
/**
* @HandleBefore()
* @return bool
* @throws ZMException
*/
public function onBefore(): bool {
$r = ctx()->getUserId(); // 从上下文获取发消息的用户 QQ
$admin_list = LightCache::get("admin_list") ?? []; // 从轻量缓存获取管理员列表
return in_array($r, $admin_list); // 返回这个 QQ 是否在管理员列表中
}
}
```
其中,`@MiddlewareClass("admin")` 的意思是,定义这个类为名字叫 `admin` 的中间件,同时,所有中间件的类**必须**带上 `implements MiddlewareInterface`,统一接口形式。
`@HandleBefore()` 代表的是这个类下的这个函数onBefore被标注为这个中间件的 `onBefore` 事件,也就是说,如果有别的注解事件插入了这个 `admin` 中间件,那么执行对应注解事件前都要执行一下 `@HandleBefore` 所绑定的这个函数。而这个绑定的函数只能返回 `bool` 类型的值哦!
## 第二步:使用中间件
使用中间件很简单,在需要阻断的注解事件绑定的函数上再加一个注解就好了!我们以上方的禁言例子说明:
```php
/**
* @Middleware("admin")
* @CQCommand(match="禁言",message_type="group")
*/
```
<chat-box>
^ 假设我是管理员
) 禁言 1234567 600
( 禁言成功!
^ 假设我不在管理员名单里
) 禁言 1234567 900
^ 机器人没有回复,因为中间件返回了 false不继续执行
</chat-box>
而这时候有朋友又要问了,如果我有一系列管理员命令,假设都在一个叫 `AdminFunc.php` 的模块类里,我是不是还得一个一个地给注解事件写 `@Middleware("admin")` 呢?当然不需要!如果你这个类所有的注解事件都是机器人的聊天事件(`@CQCommand``@CQMessage`)的话,可以直接给类注解这个中间件,效果等同于给每一个函数写一次中间件注解。
```php
<?php
namespace Module\Example;
use ZM\Annotation\Http\Middleware;
/**
* Class AdminFunc
* @package Module\Example
* @Middleware("admin")
*/
class AdminFunc
{
// ...这里是你的一堆注解事件的函数
}
```
## 第三步:补全代码
上面我们讲到了,中间件里面使用了 `LightCache` 轻量缓存来储存临时的管理员列表,那么我们将这部分的代码完善吧!
=== "src/Module/Example/AdminFunc.php"
```php
<?php
namespace Module\Example;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\Middleware;
use ZM\API\CQ;
/**
* Class AdminFunc
* @package Module\Example
* @Middleware("admin")
*/
class AdminFunc
{
/**
* @CQCommand(match="禁言",message_type="group")
*/
public function banSomeone() {
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
$r2 = ctx()->getFullArg("请输入禁言的时间(秒)");
$cq = CQ::getCQ($r1);
if ($cq !== null) {
if ($cq["type"] != "at") return "请at或者输入正确的QQ号";
$r1 = $cq["params"]["qq"];
}
// 群内禁言用户
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2);
return "禁言成功!";
}
/**
* @CQCommand(match="解除禁言",message_type="group")
*/
public function unbanSomeone() {
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
$cq = CQ::getCQ($r1);
if ($cq !== null) {
if ($cq["type"] != "at") return "请at或者输入正确的QQ号";
$r1 = $cq["params"]["qq"];
}
// 群内禁言用户
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, 0);
return "解除禁言成功!";
}
}
```
=== "src/Module/Example/AdminManager.php"
```php
<?php
namespace Module\Example;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Swoole\OnStart;
use ZM\Store\LightCache;
use ZM\Store\Lock\SpinLock;
class AdminManager
{
/**
* @OnStart()
*/
public function onStart() {
if (!LightCache::isset("admin_list")) { //一次性代码首次执行才会执行if
LightCache::set("admin_list", [ // 框架启动时初始化管理员列表
"123456",
"234567"
], -2); // 这里用 -2 的原因是将这一列表持久化保存,避免关闭框架后丢失
}
}
/**
* @CQCommand(match="添加管理员")
* @Middleware("admin")
*/
public function addAdmin() { //只有管理员才能添加管理员
$qq = ctx()->getNextArg("请输入要添加管理员的QQ(qq号码不可at");
SpinLock::lock("admin_list"); //如果是多进程模式的话需要加锁
$ls = LightCache::get("admin_list");
if (!in_array($qq, $ls)) $ls[] = $qq;
LightCache::set("admin_list", $ls, -2);
SpinLock::unlock("admin_list"); //如果是多进程模式的话需要加锁
return "成功添加 $qq 到管理员列表!";
}
}
```
<chat-box>
^ 现在我是 123456
) 禁言 13579 60
( 禁言成功!
) 解除禁言 13579
( 解除禁言成功!
) 添加管理员 98765
( 成功添加 98765 到管理员列表!
^ 现在我是98765
) 禁言 13579
( 请输入禁言的时间(秒)
) 120
( 禁言成功!
</chat-box>

View File

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

View File

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

View File

@@ -0,0 +1,70 @@
# 框架多进程
首先对于多进程概念,对于传统 PHP 程序员可能比较陌生,唯一接触到的地方可能就是 php-fpm 等一些方式处理时间长的请求时开进程去执行。关于多进程,我觉得廖雪峰的 Python 多进程这段讲的不错:
> Unix/Linux 操作系统提供了一个`fork()`系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是`fork()`调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
这里面的重点在于,多进程的创建,是父进程的复制,然后两个进程接下来运行的代码和存的内容就分道扬镳了。
PHP 也是如此,框架的多进程又是怎么一回事呢?为什么要采用多进程呢?
## 作用
使用过框架的你一定知道,框架是以命令行方式运行 PHP 的,而命令行方式运行 PHP就代表要常驻内存就像 Python、Node.js 一样。而默认情况下,比如 Python 的 Flask 为单线程单进程模式,也就是说同时只能处理一个 Web 请求。但大部分情况下,比如 Node.js提供的都是异步 I/O这也就是说明它在 Web 处理请求上,可同时承接的 I/O 密集型请求会更多一些,这样在对一般的 Web 应用中 I/O 密集型场景非常有用,而且往往只需要单进程也可以承载上万的并发请求。
在炸毛框架中,因为框架基于 Swoole 构建,所以天然支持协程,而协程就是针对 I/O 操作进行一个调度,类似异步的 Node.js所以针对项目中存在太多的 SQL 语句执行、文件读写的话,炸毛框架直接上手,无需做任何修改,也可以达到很好的性能。
**但是**CPU 密集型的应用怎么办呢?假设我的 Web 应用有大量的排序、md5 运算怎么办呢?这样的阻塞,假设是一个超级大的 for 循环或者是要执行很长时间的 while 循环CPU 一直在被占用。多进程就是针对 CPU 密集型的应用说 yes 的一个方案。
![Untitled Diagram (1)](../assets/img/single-process.png)
我们假设现在有 3 个请求同时访问,也就是说上面的流程需要执行 3 遍。而如果我们只有一个进程的话,最后一个请求需要等待的时间为 `2*3+5*3=21` 秒,非常耗时。
而如果有两个进程处理 3 个请求,则最后一个完成的请求就缩短了,`2+5+2+5=14` 秒。
![Untitled Diagram (2)](../assets/img/Untitled Diagram (2).png)
所以如果要充分利用你的服务器或者个人电脑的多核 CPU 资源,就要设置多个进程来处理。一个进程只能在一个 CPU 上运行,而设置了多进程后,就可以让多核 CPU 充分运行多个进程,所以我们给框架设置多进程的推荐数值为等同于 CPU 的核心数。
## 为什么不是多线程
因为众所周知PHP 对线程的支持比较不好,而 ZTS 版本的 PHP 又会影响传统的 Web 端 PHP 的性能,再加上 Linux 对线程的切换效率和多进程切换的效率差不多,多线程容易造成数据读写不安全等问题,故 Swoole 使用的是多进程模型。
## 框架进程模型
![Untitled Diagram (3)](../assets/img/Untitled Diagram (3).png)
上图中,横向的时间片可以理解为并行执行,这些操作在多个 CPU 内可能同时在执行。
## 进程间隔离
众所周知,进程是程序在操作系统中的一个边界,和自己有关的一切变量、内容和代码都在自己的进程内,不同进程之间如果不使用管道等方式,是不可以互相访问的。而加上开始描述的,创建子进程是一个复制自身的过程,所以也就会有如下图的情况:
![Untitled Diagram (4)](../assets/img/Untitled Diagram (4).png)
我们以静态类为例,设置一个进程中的全局变量。这里就会出现,同一个静态变量在多个进程中完全不同的值的结果。此后,我们将会在 Worker 进程中执行用户的代码,如果设置 Worker 数量仅为 1 的话,那么就简单许多了,你还是可以使用全局变量或静态类来存储你想要的内容而不用担心这种多个进程变量隔离的情况(因为用户的 Web 请求处理的代码只会在一个 Worker 进程中执行)。如果像上图一样设置了多个 Worker则用户过来的比如 HTTP 请求就有可能出现在不同的 Worker 进程中,给全局变量设值就一定会造成不同步的问题。这时我们就不可以使用全局变量做数据同步(注意,我说的是数据同步)。
## 跨进程同步
跨进程同步方案中,框架给出了很多种解决方案。
- MySQL 数据库
- Redis
- LightCache 轻量缓存(共享内存)
- WorkerCache 大缓存
- ZMAtomic 跨进程原子计数器
下面的表格我将列出下方的特点和各自的优缺点:
| 类型 | 用途 | 优点 | 缺点 |
| ----------- | --------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
| MySQL | 大型的传统的关系式数据都可以用数据库,你懂的 | 就是数据库的优点 | 和数据库不在同一台服务器的话网络延迟会较大,数据获取效率不高 |
| Redis | 传统的 key-value 数据库 | 数据无同步等问题,性能高 | 有网络通信延迟 |
| LightCache | 框架封装的跨进程的 key-value 存储模型 | 性能强悍,无 I/O 和网络通信 | 需要提前分配最大内存大小,最大单个值长度大小,不灵活 |
| WorkerCache | 框架封装的基于进程的 key-value 存储模型,类似 Redis | 无需提前分配最大内存大小,受限于 PHP memory_limit | 见 WorkerCache 的说明 |
!!! note "WorkerCache 的说明"
对于 WorkerCache 来说其实是比较特殊的进程间通信。具体来说就是WorkerCache 的原理就是将变量指定的存到一个进程中,如果是本进程读写的话直接相当于改一下全局变量,如果是其他进程读写的话,则依靠进程间通信。
所以缺点也显而易见,如果使用过程中不是命中了 WorkerCache 存储所在的进程的话,则一直会使用进程间通信,影响一定的效率。

224
docs/assets/face_id.html Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -158,4 +158,20 @@ color green 我是绿色的字
在 1.4 版本开始,框架支持启动时的 motd 内容修改。
文件位置:`config/motd.txt`
文件位置:`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 带有任何颜色,使用无色主题
```

View File

@@ -114,7 +114,7 @@ public function ping() {
## getConnection() - WS 连接对象
返回此上下文相关联的 WebSocket 连接对象。
返回此上下文相关联的 WebSocket 连接对象。详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client)。
可以使用的事件:所有 **getFrame()** 可以使用的都可以使用。

View File

@@ -82,15 +82,20 @@ class Hello {
CQ 码字符反转义。
定义:`CQ::encode($msg, $is_content = false)`
`$is_content` 为 true 时,会将 `&#44;` 转义为 `,`
| 反转义前 | 反转义后 |
| -------- | -------- |
| `&amp;` | `&` |
| `&#91;` | `[` |
| `&#93;` | `]` |
| `&#44;` | `,` |
```php
$str = CQ::decode("&#91;我只是一条普通的文本&#93;");
// 转换为 "[我只是一条普通的文本]"
$str = CQ::decode("&#91;CQ:at,qq=我只是一条普通的文本&#93;");
// 转换为 "[CQ:at,qq=我只是一条普通的文本]"
```
### CQ::encode()
@@ -102,6 +107,14 @@ $str = CQ::encode("[CQ:我只是一条普通的文本]");
// $str: "&#91;CQ:我只是一条普通的文本&#93;"
```
定义:`CQ::encode($msg, $is_content = false)`
`$is_content` 为 true 时,会将 `,` 转义为 `&#44;`
### CQ::escape()
`CQ::encode()`
### CQ::removeCQ()
去除字符串中所有的 CQ 码。
@@ -111,6 +124,48 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
// $str: "这是带表情的全体消息"
```
### CQ::getCQ()
解析 CQ 码。
- 参数:`getCQ($msg);`:要解析出 CQ 码的消息。
- 返回:`数组 | null`,见下表
| 键名 | 说明 |
| ------ | ------------------------------------------------------------ |
| type | CQ码类型比如 `[CQ:at]` 中的 `at` |
| params | 参数列表,比如 `[CQ:image,file=123.jpg,url=http://a.com/a.jpg]`params 为 `["file" => "123","url" => "http://a.com/a.jpg"]` |
| start | 此 CQ 码在字符串中的起始位置 |
| end | 此 CQ 码在字符串中的结束位置 |
### CQ::getAllCQ()
解析 CQ 码,和 `getCQ()` 的区别是,这个会将字符串中的所有 CQ 码都解析出来,并以同样上方解析出来的数组格式返回。
```php
CQ::getAllCQ("[CQ:at,qq=123]你好啊[CQ:at,qq=456]");
/*
[
[
"type" => "at",
"params" => [
"qq" => "123",
],
"start" => 0,
"end" => 13,
],
[
"type" => "at",
"params" => [
"qq" => "456",
],
"start" => 17,
"end" => 30,
],
]
*/
```
## CQ 码列表
### CQ::face() - 发送 QQ 表情
@@ -119,7 +174,7 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
定义:`CQ::face($id)`
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [炸毛框架 1.x 版本文档](https://docs-v1.zhamao.xin/face_list.html)。
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](/assets/face_id.html)。
```php
/**
@@ -449,11 +504,31 @@ public function xmlTest() {
发送 QQ 兼容的 JSON 多媒体消息。
定义:`CQ::json($data)`
定义:`CQ::json($data, $resid = 0)`
参数同上,内含 JSON 字符串即可。
其中 `$resid` 是面向 go-cqhttp 扩展的参数,默认不填为 0走小程序通道填了走富文本通道发送。
!!! tip "提示"
因为某些众所周知的原因XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。
### CQ::_custom() - 扩展自定义 CQ 码
用于兼容各类含有被支持的扩展 CQ 码,比如 go-cqhttp 的 `[CQ:gift]` 礼物类型。
定义:`CQ::_custom(string $type_name, array $params)`
| 参数名 | 说明 |
| ----------- | --------------------------------------------------- |
| `type_name` | CQ 码类型,如 `music``at` |
| `params` | 发送的 CQ 码中的参数数组,例如 `["qq" => "123456"]` |
下面是一个例子:
```php
CQ::_custom("at",["qq" => "123456","qwe" => "asd"]);
// 返回:[CQ:at,qq=123456,qwe=asd]
```

View File

@@ -224,3 +224,97 @@ public function test() {
解决这一问题,就需要用到锁。这种情况下,我们首先考虑的是自旋锁,框架也因此内置了一个方便使用的自旋锁组件。详见下一章:自旋锁。
## 如何临时缓存大变量
由于 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()->getNextArg("请输入要设置的内容名称");
$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>

View File

@@ -2,20 +2,171 @@
框架核心注解事件区别于机器人和路由注解事件,这里框架注解事件都是**直接**或封装调用 Swoole 的回调事件的,所以对一些比较底层或者基础的操作都在这里做,例如收到 HTTP 或 WebSocket 连接后执行的事件函数。
## OnSwooleEvent()
## OnOpenEvent()
绑定 Swoole 所相关的事件,例如 WebSocket 接入、收到 WS 消息、关闭 WS 连接HTTP 请求到达等。这个是旧的统一的 Swoole 事件分发注解。请尽量使用上面几个新的注解
当有 WebSocket 连接接入框架时,触发注解事件
### 属性
| 类型 | 值 |
| ------------ | ------------------------------------------ |
| 名称 | `@OnSwooleEvent` |
| 触发前提 | 当参数指定的 `type` 对应的事件被触发后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnSwooleEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
| 注解绑定参数 | |
| 类型 | 值 |
| ---------- | ------------------------------------------- |
| 名称 | `@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` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
@@ -27,20 +178,81 @@
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/inside-class/) 类型,返回一个当前 WS 连接的连接对象。
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
### 示例1机器人连接框架后输出信息
## 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\Annotation\Swoole\OnOpenEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
class Hello {
/**
* 在机器人客户端连接框架后向终端输出信息
* @OnSwooleEvent("open",rule="connectIsQQ()")
* @OnOpenEvent("qq")
* @param $conn
*/
public function onConnect(ConnectionObject $conn) {
@@ -51,7 +263,7 @@ class Hello {
这里的 Console 是终端输出组件,详情见组件一栏对应的文档查询。
### 示例2阻断 Chrome 访问框架时多访问一次的问题)
## 示例2阻断 Chrome 访问框架时多访问一次的问题)
```php
<?php
@@ -61,7 +273,7 @@ use ZM\Event\EventDispatcher;
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();
@@ -69,4 +281,107 @@ class Hello {
}
```
其中 EventDispatcher 为事件分发器interrupt 是通用阻断方法,如果你平常只使用阻断,则只需掌握这一个方法即可,`EventDispatcher::interrupt()` 在所有事件内可用。
其中 EventDispatcher 为事件分发器interrupt 是通用阻断方法,如果你平常只使用阻断,则只需掌握这一个方法即可,`EventDispatcher::interrupt()` 在所有事件内可用。
## 示例3接收 WS 客户端发来的数据)
见 [接入 WebSocket 客户端](/advanced/connect-ws-client/)。
## 示例4使用 OnStart 给所有 Worker 进程写入缓存提速)
如果你有一些数据存到了文件、数据库中,且是只读不写的,那么就可以使用此方法将这个文件或者数据库的内容读入 Worker 进程的内存中进行使用来提速。
假设我们有一个大文件 json里面存着一份题库例如
```json
{
"0": {
"question": "法的调整对象是( )。",
"answer": {
"A": "行为关系",
"B": "思想关系",
"C": "利益关系",
"D": "各种社会资源"
},
"key": "A",
"answer_type": 0
},
"1": {
"question": "法律与其他社会规范的区别在于( )。",
"answer": {
"A": "是调整人们行为的规范",
"B": "有约束力",
"C": "由国家强制力保证执行",
"D": "规定制裁措施"
},
"key": "C",
"answer_type": 0
},
.....
}
```
那么我们可以使用 OnStart 来实现一个,将此文件读取到每个 Worker 进程中,并且快速取用的功能(以下做了一个简单的查题功能):
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\CQ\CQCommand;
use ZM\Console\Console;
class Hello {
public static $tiku = [];
/**
* @OnStart(-1)
*/
public function onStart() { // 注意,此函数将会在每个 Worker 执行一次
$file = file_get_contents("tiku.json"); //从文件读取json
$json = json_decode($file, true); //json解析
Hello::$tiku = $json; //将解析后的数组以静态变量的方式存到每个 Worker 的内存中
Console::success("加载题库完成!");
}
/**
* @CQCommand("找题")
*/
public function findQuestion() {
$tiku_id = ctx()->getNumArg("请输入题目的序号");
if(!isset(Hello::$tiku[$tiku_id])) return "题目id为".$tiku_id."的题目不存在!";
$timu = Hello::$tiku[$tiku_id];
$msg = "题目名称:".$timu["question"];
foreach($timu["answer"] as $k => $v) {
$msg .= "\n".$k.". ".$v;
}
$msg .= "\n正确答案:".$timu["key"];
return $msg;
}
}
```
终端效果:(我们假设运行框架的电脑是四核 CPU
```log
[14:28:00] [S] [#0] 加载题库完成!
[14:28:00] [S] [#2] 加载题库完成!
[14:28:00] [S] [#1] 加载题库完成!
[14:28:00] [S] [#3] 加载题库完成!
```
聊天效果:
<chat-box>
) 找题 1
( 题目名称:法律与其他社会规范的区别在于( )。\nA. 是调整人们行为的规范\nB. 有约束力\nC. 由国家强制力保证执行\nD. 规定制裁措施\n正确答案C
</chat-box>
## 示例5创建每分钟自动执行的爬虫
```php
/**
* @OnTick(tick_ms=60000,worker_id=0)
*/
public function onCrawl() {
$data = Foo::bar(); //这里是你自己写的要爬的接口等等一系列操作
LightCache::set("your_data_key_name", $data); //将爬虫数据存入 LightCache 轻量缓存
}
```

View File

@@ -163,5 +163,5 @@ public function onThrowing(?Exception $e) {
这里的 `@HandleException` 中的参数为要捕获的类名,注意这里面的类名的命名空间需要写全称,不能上面 use 再使用,否则会无法找到异常类。
`context()` 为获取当前协程空间绑定的 `request``response` 对象。
`ctx()` 为获取当前协程空间绑定的 `request``response` 对象。

View File

@@ -11,10 +11,25 @@ QQ 机器人事件是指 CQHTTP 插件发来的 Event 事件,被框架处理
事件是用户需要从 OneBot 被动接收的数据,有以下几个大类:
- [消息事件](#cqmessage),包括私聊消息、群消息等,被 [`@CQCommand`](#cqcommand)`@CQMessage` 注解处理。
- [通知事件](#cqnotice),包括群成员变动、好友变动等,被 `@CQNotice` 注解事件处理。
- [请求事件](#cqrequest),包括加群请求、加好友请求等,被 `@CQRequest` 注解事件处理。
- [元事件](#cqmetaevent),包括 OneBot 生命周期、心跳等,被 `@CQMetaEvent` 注解事件处理。
## 注解事件参照表
| 注解名称 | 类所在命名全称 | 作用 |
| ------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------ |
| [`@CQBefore`](/event/robot-annotations/#cqbefore) | `\ZM\Annotation\CQBefore` | OneBot 各类事件前触发的,可当作事件过滤器使用 |
| [`@CQAfter`](/event/robot-annotations/#cqafter) | `\ZM\Annotation\CQAfter` | OneBot 各类事件后触发的 |
| [`@CQMessage`](/event/robot-annotations/#cqmessage) | `\ZM\Annotation\CQMessage` | OneBot 中消息类事件的触发(机器人消息)事件 |
| [`@CQCommand`](/event/robot-annotations/#cqcommand) | `\ZM\Annotation\CQCommand` | OneBot 中消息类事件的触发(机器人消息)事件,但是被封装为指令型的,无需自己切割命令式 |
| [`@CQNotice`](/event/robot-annotations/#cqnotice) | `\ZM\Annotation\CQNotice` | OneBot 中通知类事件的触发(机器人消息)事件 |
| [`@CQRequest`](/event/robot-annotations/#cqrequest) | `\ZM\Annotation\CQRequest` | OneBot 中请求类事件的触发(机器人消息)事件,一般带有请求信息,可联动相关响应的 API 完成功能编写 |
| [`@CQMetaEvent`](/event/robot-annotations/#cqmetaevent) | `\ZM\Annotation\CQMetaEvent` | OneBot 中涉及 OneBot 实现本身的一些和机器人事件无关的元事件,比如 WS 连接的心跳包 |
## CQMessage()
QQ 收到消息后触发的事件对应注解。

View File

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

View File

@@ -224,5 +224,14 @@ public function repeat() {
这样,一个简易的复读机就做好了!回到 QQ 机器人聊天,向机器人发送 `echo 你好啊`,它会回复你 `你好啊`。
<chat-box>
) echo 你好啊
( 你好啊
) echo
( 请输入你要回复的内容
) 哦豁,完蛋
( 哦豁,完蛋
</chat-box>
> 如果你只回复 `echo` 的话,它会先和你进入一个会话状态,并问你 `请输入你要回复的内容`,这时你再次说一些内容例如 `哦豁`,会回复你 `哦豁`。效果和直接输入 `echo 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。

View File

@@ -4,6 +4,11 @@
> 如果是从 v1.x 版本升级到 v2.x[点我看升级指南](/advanced/to-v2/)。
!!! tip "提示"
编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request以此来贡献此文档
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人CQHTTP 对接),包含 websocket、http 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
@@ -33,7 +38,7 @@ public function index() {
首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
1. Linux 命令行基础
1. Linux 命令行(会跑 Linux 程序)
2. php 7.2+ 开发环境
3. HTTP 协议(可选)
4. OneBot 机器人聊天接口标准(可选)

View File

@@ -42,7 +42,7 @@ function setCookie(name, value) {
var Days = 30;
var exp = new Date();
exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path=/";
}
s_theme=getCookie("_theme");
@@ -75,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>';

View File

@@ -1,5 +1,57 @@
# 更新日志v2 版本)
## v2.2.4
> 更新事件2021.2.7
- 修复:终端交互导致的 ssh 断掉后 CPU 占用过高的问题
- 修复WorkerCache 在缺少配置文件下工作异常的问题
- 新增:全局函数:`zm_atomic()`
## v2.2.3
> 更新时间2021.1.30
- 修复waitMessage() 在 v2.2.2 版本中不可用的 bug
- 修复access_token 无效的问题
## 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

View File

@@ -10,8 +10,8 @@ theme:
favicon: assets/favicon.png
language: zh
palette:
primary: blue
accent: blue
primary: red
accent: red
features:
- navigation.tabs
extra_javascript:
@@ -34,7 +34,7 @@ extra:
version:
method: mike
copyright: 'Copyright &copy; 2019 - 2020 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
copyright: 'Copyright &copy; 2019 - 2021 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
<button data-md-color-scheme="default"><code>默认模式</code></button>
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
</span>
@@ -89,8 +89,14 @@ nav:
- 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
- 开发实战教程:
- 编写管理员才能触发的功能: advanced/example/admin.md
- FAQ: FAQ.md
- 更新日志:
- 更新日志v2: update/v2.md

View File

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

View File

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

View File

@@ -55,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));
// 回复用户结果
@@ -89,7 +89,7 @@ class Hello
* @return string
*/
public function paramGet($param) {
return "Your name: {$param["name"]}";
return "Hello, ".$param["name"];
}
/**

View File

@@ -45,11 +45,11 @@ class CQ
*/
public static function image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1) {
return
"[CQ:image,file=" . $file .
"[CQ:image,file=" . self::encode($file, true) .
(!$cache ? ",cache=0" : "") .
($flash ? ",type=flash" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . $timeout) : "") .
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
"]";
}
@@ -64,11 +64,11 @@ class CQ
*/
public static function record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1) {
return
"[CQ:record,file=" . $file .
"[CQ:record,file=" . self::encode($file, true) .
(!$cache ? ",cache=0" : "") .
($magic ? ",magic=1" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . $timeout) : "") .
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
"]";
}
@@ -82,10 +82,10 @@ class CQ
*/
public static function video($file, $cache = true, $proxy = true, $timeout = -1) {
return
"[CQ:video,file=" . $file .
"[CQ:video,file=" . self::encode($file, true) .
(!$cache ? ",cache=0" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . $timeout) : "") .
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
"]";
}
@@ -121,7 +121,7 @@ class CQ
* @return string
*/
public static function poke($type, $id, $name = "") {
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? ",name=$name" : "") . "]";
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? (",name=".self::encode($name, true)) : "") . "]";
}
/**
@@ -130,7 +130,7 @@ class CQ
* @return string
*/
public static function anonymous($ignore = 1) {
return "[CQ:anonymous".($ignore != 1 ? ",ignore=0" : "")."]";
return "[CQ:anonymous" . ($ignore != 1 ? ",ignore=0" : "") . "]";
}
/**
@@ -143,10 +143,10 @@ class CQ
*/
public static function share($url, $title, $content = null, $image = null) {
if ($content === null) $c = "";
else $c = ",content=" . $content;
else $c = ",content=" . self::encode($content, true);
if ($image === null) $i = "";
else $i = ",image=" . $image;
return "[CQ:share,url=" . $url . ",title=" . $title . $c . $i . "]";
else $i = ",image=" . self::encode($image, true);
return "[CQ:share,url=" . self::encode($url, true) . ",title=" . self::encode($title, true) . $c . $i . "]";
}
/**
@@ -159,8 +159,21 @@ class CQ
return "[CQ:contact,type=$type,id=$id]";
}
/**
* 发送位置
* @param $lat
* @param $lon
* @param string $title
* @param string $content
* @return string
*/
public static function location($lat, $lon, $title = "", $content = "") {
return "[CQ:location" .
",lat=".self::encode($lat, true) .
",lon=".self::encode($lon, true).
($title != "" ? (",title=".self::encode($title, true)) : "") .
($content != "" ? (",content=".self::encode($content, true)) : "") .
"]";
}
/**
@@ -193,10 +206,13 @@ class CQ
return " ";
}
if ($content === null) $c = "";
else $c = ",content=" . $content;
else $c = ",content=" . self::encode($content, true);
if ($image === null) $i = "";
else $i = ",image=" . $image;
return "[CQ:music,type=custom,url=" . $id_or_url . ",audio=" . $audio . ",title=" . $title . $c . $i . "]";
else $i = ",image=" . self::encode($image, true);
return "[CQ:music,type=custom,url=" .
self::encode($id_or_url, true) .
",audio=" . self::encode($audio, true) . ",title=" . self::encode($title, true) . $c . $i .
"]";
default:
Console::warning("传入的music type($type)错误!");
return " ";
@@ -208,19 +224,36 @@ class CQ
}
public static function node($user_id, $nickname, $content) {
return "[CQ:node,user_id=$user_id,nickname=$nickname,content=".self::escape($content)."]";
return "[CQ:node,user_id=$user_id,nickname=".self::encode($nickname, true).",content=" . self::encode($content, true) . "]";
}
public static function xml($data) {
return "[CQ:xml,data=" . self::encode($data, true) . "]";
}
public static function json($data, $resid = 0) {
return "[CQ:json,data=" . self::encode($data, true) . ",resid=" . intval($resid) . "]";
}
public static function _custom(string $type_name, $params) {
$code = "[CQ:" . $type_name;
foreach ($params as $k => $v) {
$code .= "," . $k . "=" . self::escape($v, true);
}
$code .= "]";
return $code;
}
/**
* 反转义字符串中的CQ码敏感符号
* @param $str
* @param $msg
* @param bool $is_content
* @return mixed
*/
public static function decode($str) {
$str = str_replace("&amp;", "&", $str);
$str = str_replace("&#91;", "[", $str);
$str = str_replace("&#93;", "]", $str);
return $str;
public static function decode($msg, $is_content = false) {
$msg = str_replace(["&amp;", "&#91;", "&#93;"], ["&", "[", "]"], $msg);
if ($is_content) $msg = str_replace("&#44;", ",", $msg);
return $msg;
}
public static function replace($str) {
@@ -230,42 +263,97 @@ class CQ
}
/**
* 转义CQ码
* 转义CQ码的特殊字符同encode
* @param $msg
* @param bool $is_content
* @return mixed
*/
public static function escape($msg) {
$msg = str_replace("&", "&amp;", $msg);
$msg = str_replace("[", "&#91;", $msg);
$msg = str_replace("]", "&#93;", $msg);
public static function escape($msg, $is_content = false) {
$msg = str_replace(["&", "[", "]"], ["&amp;", "&#91;", "&#93;"], $msg);
if ($is_content) $msg = str_replace(",", "&#44;", $msg);
return $msg;
}
public static function encode($str) {
return self::escape($str);
/**
* 转义CQ码的特殊字符
* @param $msg
* @param false $is_content
* @return mixed
*/
public static function encode($msg, $is_content = false) {
$msg = str_replace(["&", "[", "]"], ["&amp;", "&#91;", "&#93;"], $msg);
if ($is_content) $msg = str_replace(",", "&#44;", $msg);
return $msg;
}
/**
* 移除消息中所有的CQ码并返回移除CQ码后的消息
* @param $msg
* @return string
*/
public static function removeCQ($msg) {
while (($cq = self::getCQ($msg)) !== null) {
$msg = str_replace(mb_substr($msg, $cq["start"], $cq["end"] - $cq["start"] + 1), "", $msg);
$final = "";
$last_end = 0;
foreach(self::getAllCQ($msg) as $k => $v) {
$final .= mb_substr($msg, $last_end, $v["start"] - $last_end);
$last_end = $v["end"] + 1;
}
return $msg;
$final .= mb_substr($msg, $last_end);
return $final;
}
/**
* 获取消息中第一个CQ码
* @param $msg
* @return array|null
*/
public static function getCQ($msg) {
if (($start = mb_strpos($msg, '[')) === false) return null;
if (($end = mb_strpos($msg, ']')) === false) return null;
$msg = mb_substr($msg, $start + 1, $end - $start - 1);
if (mb_substr($msg, 0, 3) != "CQ:") return null;
$msg = mb_substr($msg, 3);
$msg2 = explode(",", $msg);
$type = array_shift($msg2);
$array = [];
foreach ($msg2 as $k => $v) {
$ss = explode("=", $v);
$sk = array_shift($ss);
$array[$sk] = implode("=", $ss);
if (($head = mb_strpos($msg, "[CQ:")) !== false) {
$key_offset = mb_substr($msg, $head);
$close = mb_strpos($key_offset, "]");
if ($close === false) return null;
$content = mb_substr($msg, $head + 4, $close + $head - mb_strlen($msg));
$exp = explode(",", $content);
$cq["type"] = array_shift($exp);
foreach ($exp as $k => $v) {
$ss = explode("=", $v);
$sk = array_shift($ss);
$cq["params"][$sk] = self::decode(implode("=", $ss), true);
}
$cq["start"] = $head;
$cq["end"] = $close + $head;
return $cq;
} else {
return null;
}
return ["type" => $type, "params" => $array, "start" => $start, "end" => $end];
}
/**
* 获取消息中所有的CQ码
* @param $msg
* @return array
*/
public static function getAllCQ($msg) {
$cqs = [];
$offset = 0;
while (($head = mb_strpos(($submsg = mb_substr($msg, $offset)), "[CQ:")) !== false) {
$key_offset = mb_substr($submsg, $head);
$tmpmsg = mb_strpos($key_offset, "]");
if ($tmpmsg === false) break; // 没闭合不算CQ码
$content = mb_substr($submsg, $head + 4, $tmpmsg + $head - mb_strlen($submsg));
$exp = explode(",", $content);
$cq = [];
$cq["type"] = array_shift($exp);
foreach ($exp as $k => $v) {
$ss = explode("=", $v);
$sk = array_shift($ss);
$cq["params"][$sk] = self::decode(implode("=", $ss), true);
}
$cq["start"] = $offset + $head;
$cq["end"] = $offset + $tmpmsg + $head;
$offset += $tmpmsg + 1;
$cqs[] = $cq;
}
return $cqs;
}
}

View File

@@ -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 {

View File

@@ -59,7 +59,7 @@ class ZMRobot
public static function getAllRobot() {
$r = ManagerGM::getAllByName('qq');
$obj = [];
foreach($r as $v) {
foreach ($r as $v) {
$obj[] = new ZMRobot($v);
}
return $obj;
@@ -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) {

View File

@@ -9,11 +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\Http\RouteManager;
use ZM\Utils\DataProvider;
class AnnotationParser
{
@@ -48,7 +47,7 @@ class AnnotationParser
*/
public function registerMods() {
foreach ($this->path_list as $path) {
Console::debug("parsing annotation in ".$path[0]);
Console::debug("parsing annotation in " . $path[0]);
$all_class = getAllClasses($path[0], $path[1]);
$this->reader = new AnnotationReader();
foreach ($all_class as $v) {
@@ -91,6 +90,7 @@ class AnnotationParser
if ($vs instanceof ErgodicAnnotation) {
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;
}

View File

@@ -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;

View File

@@ -4,7 +4,6 @@
namespace ZM\Annotation\Http;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;

View File

@@ -5,7 +5,6 @@ namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\Interfaces\Rule;
/**
* @Annotation

View File

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

View File

@@ -31,7 +31,7 @@ class BuildCommand extends Command
$target_dir = $input->getOption("target") ?? (__DIR__ . '/../../../resources/');
if (mb_strpos($target_dir, "../")) $target_dir = realpath($target_dir);
if ($target_dir === false) {
$output->writeln(TermColor::color8(31) . "Error: No such file or directory (".__DIR__ . '/../../../resources/'.")" . TermColor::RESET);
$output->writeln(TermColor::color8(31) . "Error: No such file or directory (" . __DIR__ . '/../../../resources/' . ")" . TermColor::RESET);
return Command::FAILURE;
}
$output->writeln("Target: " . $target_dir . " , Version: " . ($version = json_decode(file_get_contents(__DIR__ . "/../../../composer.json"), true)["version"]));
@@ -51,7 +51,7 @@ class BuildCommand extends Command
return Command::SUCCESS;
}
private function build ($target_dir, $filename) {
private function build($target_dir, $filename) {
@unlink($target_dir . $filename);
$phar = new Phar($target_dir . $filename);
$phar->startBuffering();

View File

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

View File

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

View File

@@ -0,0 +1,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;
}
}

View File

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

View File

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

View File

@@ -36,8 +36,8 @@ class PureHttpCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output) {
$tty_width = explode(" ", trim(exec("stty size")))[1];
if(realpath($input->getArgument('dir') ?? '.') === false) {
$output->writeln("<error>Directory error(".($input->getArgument('dir') ?? '.')."): no such file or directory.</error>");
if (realpath($input->getArgument('dir') ?? '.') === false) {
$output->writeln("<error>Directory error(" . ($input->getArgument('dir') ?? '.') . "): no such file or directory.</error>");
return self::FAILURE;
}
$global = ZMConfig::get("global");
@@ -60,15 +60,15 @@ class PureHttpCommand extends Command
"document_root" => realpath($input->getArgument('dir') ?? '.'),
"document_index" => $index
]);
echo "\r".Coroutine::stats()["coroutine_peak_num"];
echo "\r" . Coroutine::stats()["coroutine_peak_num"];
});
$server->on("start", function ($server) {
Process::signal(SIGINT, function () use ($server) {
Console::warning("Server interrupted by keyboard.");
for ($i = 0; $i < 32; ++$i) {
$num = ZMAtomic::$atomics["request"][$i]->get();
if($num != 0)
echo "[$i]: ".$num."\n";
if ($num != 0)
echo "[$i]: " . $num . "\n";
}
$server->shutdown();
$server->stop();

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,6 @@ class DB
/**
* @param $table_name
* @param bool $enable_cache
* @return Table
* @throws DbException
*/
@@ -39,7 +38,7 @@ class DB
if (Table::getTableInstance($table_name) === null) {
if (in_array($table_name, self::$table_list))
return new Table($table_name);
elseif(SqlPoolStorage::$sql_pool !== null){
elseif (SqlPoolStorage::$sql_pool !== null) {
throw new DbException("Table " . $table_name . " not exist in database.");
} else {
throw new DbException("Database connection not exist or connect failed. Please check sql configuration");
@@ -85,7 +84,7 @@ class DB
* @throws DbException
*/
public static function rawQuery(string $line, $params = [], $fetch_mode = ZM_DEFAULT_FETCH_MODE) {
Console::debug("MySQL: ".$line." | ". implode(", ", $params));
Console::debug("MySQL: " . $line . " | " . implode(", ", $params));
try {
$conn = SqlPoolStorage::$sql_pool->get();
if ($conn === false) {
@@ -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)) {
@@ -115,7 +115,7 @@ class DB
return $ps->fetchAll($fetch_mode);
}
} catch (DbException $e) {
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
if (mb_strpos($e->getMessage(), "has gone away") !== false) {
zm_sleep(0.2);
Console::warning("Gone away of MySQL! retrying!");
return self::rawQuery($line, $params);
@@ -123,7 +123,7 @@ class DB
Console::warning($e->getMessage());
throw $e;
} catch (PDOException $e) {
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
if (mb_strpos($e->getMessage(), "has gone away") !== false) {
zm_sleep(0.2);
Console::warning("Gone away of MySQL! retrying!");
return self::rawQuery($line, $params);

View File

@@ -28,6 +28,6 @@ class InsertBody
* @throws DbException
*/
public function save() {
DB::rawQuery('INSERT INTO ' . $this->table->getTableName() . ' VALUES ('.implode(',', array_fill(0, count($this->row), '?')).')', $this->row);
DB::rawQuery('INSERT INTO ' . $this->table->getTableName() . ' VALUES (' . implode(',', array_fill(0, count($this->row), '?')) . ')', $this->row);
}
}

View File

@@ -4,7 +4,6 @@
namespace ZM\DB;
class Table
{
private $table_name;
@@ -28,7 +27,7 @@ class Table
return new SelectBody($this, $what == [] ? ["*"] : $what);
}
public function where($column, $operation_or_value, $value = null){
public function where($column, $operation_or_value, $value = null) {
return (new SelectBody($this, ["*"]))->where($column, $operation_or_value, $value);
}
@@ -47,7 +46,7 @@ class Table
return new DeleteBody($this);
}
public function statement($line){
public function statement() {
$this->cache = [];
//TODO: 无返回的statement语句
}

View File

@@ -18,6 +18,7 @@ class UpdateBody
* @var array
*/
private $set_value;
/**
* UpdateBody constructor.
* @param Table $table
@@ -31,19 +32,19 @@ class UpdateBody
/**
* @throws DbException
*/
public function save(){
public function save() {
$arr = [];
$msg = [];
foreach($this->set_value as $k => $v) {
$msg []= $k .' = ?';
$arr[]=$v;
foreach ($this->set_value as $k => $v) {
$msg [] = $k . ' = ?';
$arr[] = $v;
}
if(($msg ?? []) == []) throw new DbException('update value sets can not be empty!');
$line = 'UPDATE '.$this->table->getTableName().' SET '.implode(', ', $msg);
if($this->where_thing != []) {
if (($msg ?? []) == []) throw new DbException('update value sets can not be empty!');
$line = 'UPDATE ' . $this->table->getTableName() . ' SET ' . implode(', ', $msg);
if ($this->where_thing != []) {
list($sql, $param) = $this->getWhereSQL();
$arr = array_merge($arr, $param);
$line .= ' WHERE '.$sql;
$line .= ' WHERE ' . $sql;
}
return DB::rawQuery($line, $arr);
}

View File

@@ -15,17 +15,17 @@ trait WhereBody
return $this;
}
protected function getWhereSQL(){
protected function getWhereSQL() {
$param = [];
$msg = '';
foreach($this->where_thing as $k => $v) {
foreach($v as $ks => $vs) {
if($param != []) {
$msg .= ' AND '.$ks ." $k ?";
foreach ($this->where_thing as $k => $v) {
foreach ($v as $ks => $vs) {
if ($param != []) {
$msg .= ' AND ' . $ks . " $k ?";
} else {
$msg .= "$ks $k ?";
}
$param []=$vs;
$param [] = $vs;
}
}
if ($msg == '') $msg = 1;

View File

@@ -87,27 +87,23 @@ class EventDispatcher
/**
* @param mixed ...$params
* @throws Exception
* @throws InterruptException
*/
public function dispatchEvents(...$params) {
try {
foreach ((EventManager::$events[$this->class] ?? []) as $v) {
$this->dispatchEvent($v, $this->rule, ...$params);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。");
if($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
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)($this->store);
}
}
if($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
if ($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
} catch (InterruptException $e) {
$this->store = $e->return_var;
$this->status = self::STATUS_INTERRUPTED;
} catch (Exception $e) {
$this->status = self::STATUS_EXCEPTION;
throw $e;
} catch (Error $e) {
} catch (Exception | Error $e) {
$this->status = self::STATUS_EXCEPTION;
throw $e;
}
@@ -117,9 +113,9 @@ class EventDispatcher
* @param mixed $v
* @param null $rule_func
* @param mixed ...$params
* @throws AnnotationException
* @throws InterruptException
* @return bool
* @throws InterruptException
* @throws AnnotationException
*/
public function dispatchEvent($v, $rule_func = null, ...$params) {
$q_c = $v->class;

View File

@@ -9,8 +9,11 @@ use Exception;
use Swoole\Timer;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnSave;
use ZM\Annotation\Swoole\OnTick;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Store\LightCache;
use ZM\Store\ZMAtomic;
class EventManager
@@ -38,7 +41,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);
@@ -59,5 +62,11 @@ class EventManager
}
});
}
$conf = ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if (server()->worker_id == $conf["worker"]) {
zm_timer_tick(ZMConfig::get("global", "light_cache")["auto_save_interval"] * 1000, function () {
LightCache::savePersistence();
});
}
}
}

View File

@@ -1,4 +1,6 @@
<?php /** @noinspection PhpComposerExtensionStubsInspection */
<?php /** @noinspection PhpUnreachableStatementInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
namespace ZM\Event;
@@ -14,12 +16,12 @@ 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;
@@ -39,9 +41,11 @@ use ZM\Exception\InterruptException;
use ZM\Framework;
use ZM\Http\Response;
use ZM\Module\QQBot;
use ZM\Store\LightCache;
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;
@@ -59,7 +63,12 @@ class ServerEventHandler
if ($terminal_id !== null) {
ZMBuf::$terminal = $r = STDIN;
Event::add($r, function () use ($r) {
$var = trim(fgets($r));
$fget = fgets($r);
if ($fget === false) {
Event::del($r);
return;
}
$var = trim($fget);
try {
Terminal::executeCommand($var, $r);
} catch (Exception $e) {
@@ -71,11 +80,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.");
@@ -84,11 +100,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.");
}
}
}
@@ -106,6 +122,9 @@ class ServerEventHandler
* @param $worker_id
*/
public function onWorkerStop(Server $server, $worker_id) {
if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) {
LightCache::savePersistence();
}
Console::debug(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止");
}
@@ -228,7 +247,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初始化的内容部分
@@ -245,7 +264,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);
}
}
}
@@ -258,11 +277,12 @@ 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[\Swoole\Coroutine::getCid()]);
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() . ";");
});
@@ -303,6 +323,9 @@ class ServerEventHandler
*/
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]);
@@ -386,7 +409,15 @@ 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"] ?? "");
$access_token = explode(" ", $request->header["authorization"] ?? $request->get["token"] ?? "")[1] ?? "";
if (($a = ZMConfig::get("global", "access_token")) != "") {
if ($access_token !== $a) {
$server->close($request->fd);
Console::warning("Unauthorized access_token: " . $access_token);
return;
}
}
$type_conn = ManagerGM::getTypeClassName($type);
ManagerGM::pushConnect($request->fd, $type_conn);
$conn = ManagerGM::get($request->fd);
@@ -477,9 +508,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);
@@ -490,28 +522,80 @@ 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 "hasKeyWorkerCache":
$r = WorkerCache::hasKey($data["key"], $data["subkey"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
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;
}
/**

View File

@@ -94,7 +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 (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"];
}
@@ -250,6 +250,7 @@ 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;
}

View File

@@ -4,8 +4,6 @@
namespace ZM\Http;
use ZM\Console\Console;
class Response
{
@@ -162,7 +160,7 @@ class Response
* @return mixed
*/
public function end($content = null) {
if(!$this->is_end) {
if (!$this->is_end) {
$this->is_end = true;
return $this->response->end($content);
} else {

View File

@@ -8,6 +8,7 @@ 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
{
@@ -15,7 +16,7 @@ class RouteManager
public static $routes = null;
public static function importRouteByAnnotation(RequestMapping $vss, $method, $class, $methods_annotations) {
if(self::$routes === null) self::$routes = new RouteCollection();
if (self::$routes === null) self::$routes = new RouteCollection();
// 拿到所属方法的类上面有没有控制器的注解
$prefix = '';
@@ -25,8 +26,9 @@ class RouteManager
break;
}
}
$route_name = $prefix."/".$vss->route;
$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);

View File

@@ -13,14 +13,14 @@ class StaticFileHandler
public function __construct($filename, $path) {
$full_path = realpath($path . "/" . $filename);
$response = ctx()->getResponse();
Console::debug("Full path: ".$full_path);
Console::debug("Full path: " . $full_path);
if ($full_path !== false) {
if (strpos($full_path, $path) !== 0) {
$response->status(403);
$response->end("403 Forbidden");
return true;
} else {
if(is_file($full_path)) {
if (is_file($full_path)) {
$exp = strtolower(pathinfo($full_path)['extension'] ?? "unknown");
$response->setHeader("Content-Type", ZMConfig::get("file_header")[$exp] ?? "application/octet-stream");
$response->end(file_get_contents($full_path));

View File

@@ -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,27 +24,27 @@ class QQBot
{
/**
* @throws InterruptException
* @throws Exception
*/
public function handle() {
try {
$data = json_decode(context()->getFrame()->data, true);
set_coroutine_params(["data" => $data]);
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());
if ($data["post_type"] != "meta_event") {
$r = $this->dispatchBeforeEvents($data); // before在这里执行元事件不执行before为减少不必要的调试日志
if ($r->store === "block") EventDispatcher::interrupt();
}
if (CoMessage::resumeByWS()) {
EventDispatcher::interrupt();
}
//Console::warning("最上数据包:".json_encode($data));
$this->dispatchEvents($data);
} else {
$this->dispatchAPIResponse($data);
}
if (isset($data["echo"]) || isset($data["post_type"])) {
if (CoMessage::resumeByWS()) EventDispatcher::interrupt();
}
if (isset($data["post_type"])) $this->dispatchEvents($data);
else $this->dispatchAPIResponse($data);
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) {
$e->module->finalReply($e->getMessage());
}
@@ -55,7 +53,7 @@ class QQBot
/**
* @param $data
* @return EventDispatcher
* @throws InterruptException
* @throws Exception
*/
public function dispatchBeforeEvents($data) {
$before = new EventDispatcher(CQBefore::class);
@@ -78,6 +76,7 @@ class QQBot
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));
@@ -176,45 +175,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);
}
}

View File

@@ -6,7 +6,11 @@ namespace ZM\Store;
use Exception;
use Swoole\Table;
use ZM\Annotation\Swoole\OnSave;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Event\EventDispatcher;
use ZM\Exception\ZMException;
class LightCache
{
@@ -19,6 +23,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 +59,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 +72,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 +86,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 +102,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 +119,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 +132,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;
@@ -169,7 +178,16 @@ class LightCache
return $r;
}
public static function savePersistence() {
public static function savePersistence($only_worker = false) {
// 下面将OnSave激活一下
if (server()->worker_id == (ZMConfig::get("global", "worker_cache")["worker"] ?? 0)) {
$dispatcher = new EventDispatcher(OnSave::class);
$dispatcher->dispatchEvents();
}
if($only_worker) return;
if (self::$kv_table === null) return;
$r = [];
foreach (self::$kv_table as $k => $v) {
@@ -178,11 +196,13 @@ class LightCache
$r[$k] = self::parseGet($v);
}
}
if(self::$config["persistence_path"] == "") return;
if (self::$config["persistence_path"] == "") return;
if (file_exists(self::$config["persistence_path"])) {
$r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256));
if ($r === false) Console::error("Not saved, please check your \"persistence_path\"!");
}
}
private static function checkExpire($key) {

View File

@@ -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;
}
/**

View File

@@ -15,18 +15,16 @@ class SpinLock
private static $delay = 1;
public static function init($key_cnt, $delay = 1)
{
public static function init($key_cnt, $delay = 1) {
self::$kv_lock = new Table($key_cnt, 0.7);
self::$delay = $delay;
self::$kv_lock->column('lock_num', Table::TYPE_INT, 8);
return self::$kv_lock->create();
}
public static function lock(string $key)
{
public static function lock(string $key) {
while (($r = self::$kv_lock->incr($key, 'lock_num')) > 1) { //此资源已经被锁上了
if(Coroutine::getCid() != -1) System::sleep(self::$delay / 1000);
if (Coroutine::getCid() != -1) System::sleep(self::$delay / 1000);
else usleep(self::$delay * 1000);
}
}
@@ -41,4 +39,10 @@ class SpinLock
public static function unlock(string $key) {
return self::$kv_lock->set($key, ['lock_num' => 0]);
}
public static function transaction(string $key, callable $function) {
SpinLock::lock($key);
$function();
SpinLock::unlock($key);
}
}

View File

@@ -16,7 +16,7 @@ class ZMRedis
* @throws NotInitializedException
*/
public static function call(callable $callable) {
if(ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
if (ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
$r = ZMRedisPool::$pool->get();
$result = $callable($r);
if (isset($r->wasted)) ZMRedisPool::$pool->put(null);
@@ -29,7 +29,7 @@ class ZMRedis
* @throws NotInitializedException
*/
public function __construct() {
if(ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
if (ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
$this->conn = ZMRedisPool::$pool->get();
}

View File

@@ -24,13 +24,13 @@ class ZMRedisPool
);
try {
$r = self::$pool->get()->ping('123');
if(strpos(strtolower($r), "123") !== false) {
if (strpos(strtolower($r), "123") !== false) {
Console::debug("成功连接redis连接池");
} else {
var_dump($r);
}
} catch (RedisException $e) {
Console::error("Redis init failed! ".$e->getMessage());
Console::error("Redis init failed! " . $e->getMessage());
self::$pool = null;
}
}

View File

@@ -0,0 +1,96 @@
<?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") ?? ["worker" => 0];
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);
}
}
public static function hasKey($key, $subkey) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if ($config["worker"] === server()->worker_id) {
return isset(self::$store[$key][$subkey]);
} else {
$action = ["hasKeyWorkerCache", "key" => $key, "subkey" => $subkey, "cid" => zm_cid()];
return self::processRemote($action, false, $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") ?? ["worker" => 0];
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") ?? ["worker" => 0];
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") ?? ["worker" => 0];
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);
}
}
}

View File

@@ -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) {
@@ -55,20 +55,21 @@ class CoMessage
SpinLock::lock("wait_api");
$all = LightCacheInside::get("wait_api", "wait_api") ?? [];
foreach ($all as $k => $v) {
if(!isset($v["compare"])) continue;
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;
}
}
$last = $k;
}
if($last !== null) {
if ($last !== null) {
$all[$last]["result"] = $dat;
LightCacheInside::set("wait_api", "wait_api", $all);
SpinLock::unlock("wait_api");
if ($all[$last]["worker_id"] != server()->worker_id) {
ZMUtil::sendActionToWorker($all[$k]["worker_id"], "resume_ws_message", $all[$last]);
ZMUtil::sendActionToWorker($all[$last]["worker_id"], "resume_ws_message", $all[$last]);
} else {
Co::resume($all[$last]["coroutine"]);
}

View File

@@ -5,6 +5,7 @@ namespace ZM\Utils;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
class DataProvider
{
@@ -15,7 +16,7 @@ class DataProvider
}
public static function getWorkingDir() {
if(LOAD_MODE == 0) return WORKING_DIR;
if (LOAD_MODE == 0) return WORKING_DIR;
elseif (LOAD_MODE == 1) return LOAD_MODE_COMPOSER_PATH;
elseif (LOAD_MODE == 2) return realpath('.');
return null;
@@ -28,4 +29,29 @@ class DataProvider
public static function getDataFolder() {
return ZM_DATA;
}
public static function saveToJson($filename, $file_array) {
$path = ZMConfig::get("global", "config_dir");
$r = explode("/", $filename);
if(count($r) == 2) {
$path = $path . $r[0]."/";
if(!is_dir($path)) mkdir($path);
$name = $r[1];
} elseif (count($r) != 1) {
Console::warning("存储失败,文件名只能有一级目录");
return false;
} else {
$name = $r[0];
}
return file_put_contents($path.$name.".json", json_encode($file_array, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
public static function loadFromJson($filename) {
$path = ZMConfig::get("global", "config_dir");
if(file_exists($path.$filename.".json")) {
return json_decode(file_get_contents($path.$filename.".json"), true);
} else {
return null;
}
}
}

View File

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

View File

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

View File

@@ -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,13 +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)) {
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;
}
@@ -265,10 +267,14 @@ function zm_timer_after($ms, callable $callable) {
}
function zm_timer_tick($ms, callable $callable) {
go(function () use ($ms, $callable) {
Console::debug("Adding extra timer tick of " . $ms . " ms");
Swoole\Timer::tick($ms, $callable);
});
if (zm_cid() === -1) {
return go(function () use ($ms, $callable) {
Console::debug("Adding extra timer tick of " . $ms . " ms");
Swoole\Timer::tick($ms, $callable);
});
} else {
return Swoole\Timer::tick($ms, $callable);
}
}
function zm_data_hash($v) {
@@ -308,3 +314,7 @@ function getAllFdByConnectType(string $type = 'default'): array {
}
return $fds;
}
function zm_atomic($name) {
return \ZM\Store\ZMAtomic::get($name);
}

10
test/usage_test.php Normal file
View File

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