Compare commits

...

45 Commits
2.5.1 ... 2.6.1

Author SHA1 Message Date
fb528d30ce update docs 2021-11-16 16:51:05 +08:00
5f2d5ed334 update to build 428 2021-11-16 16:49:32 +08:00
c20e459900 update docs 2021-11-16 15:44:34 +08:00
09220825cf update to build 427 2021-11-16 15:41:01 +08:00
Jerry Ma
3d4db23d27 Update v2.md 2021-11-10 14:10:08 +08:00
Jerry Ma
4496b67dcc Update build-update.md 2021-11-10 14:09:18 +08:00
Jerry Ma
2a13298384 update to 2.5.8 (build 426) 2021-11-10 14:07:21 +08:00
Jerry Ma
d6ec404d76 Merge pull request #52 from YuFengZe/master
Update CQ.php
2021-11-10 14:06:13 +08:00
YuFengZe
3235fd4dc1 Update CQ.php 2021-11-08 22:05:41 +08:00
crazywhalecc
293740fee2 update to 2.5.7 (build 425) 2021-11-03 23:26:43 +08:00
crazywhalecc
d3c420ec84 update to build 424 (2.6.0-alpha1) 2021-11-02 16:01:24 +08:00
crazywhalecc
85ef09d43c Merge remote-tracking branch 'origin/master' 2021-11-01 00:29:11 +08:00
Jerry Ma
e4561d69c4 Merge pull request #45 from YuFengZe/master
Bug Fixed
2021-10-31 22:50:49 +08:00
Jerry Ma
6b4d206099 Update Hello.php 2021-10-31 22:50:12 +08:00
YuFengZe
50843edf6a Bug Fixed
解决发送“我是谁”却返回机器人信息的奇怪问题。
2021-10-31 22:01:14 +08:00
crazywhalecc
3a1686f8da update docs 2021-10-18 22:24:28 +08:00
crazywhalecc
66dd91bb97 update to 2.5.6 (build 423) 2021-10-17 23:55:02 +08:00
crazywhalecc
e020e5d593 update docs 2021-10-17 17:00:33 +08:00
crazywhalecc
3d62663281 update docs 2021-10-17 16:56:52 +08:00
crazywhalecc
8d9485c02e update docs 2021-10-17 15:37:55 +08:00
Jerry Ma
9fb45dd683 Update README.md 2021-10-15 09:38:07 +08:00
Jerry Ma
8a4924dba9 Update light-cache.md 2021-10-08 11:41:40 +08:00
crazywhalecc
beaf7be606 update to version 2.5.5 (buid 422) 2021-10-06 18:01:40 +08:00
Jerry Ma
7dbd21bdf4 Merge pull request #44 from furleywolf/patch-1
更新message-util的文档
2021-09-15 12:46:48 +08:00
furleywolf
9ce3056203 更新message-util的文档
发现漏掉了一个方法,还是补上去吧
2021-09-13 17:35:39 +08:00
crazywhalecc
880b4e847c update to version 2.5.4 (buid 421) 2021-09-11 16:27:16 +08:00
crazywhalecc
71e83d5bc8 update docs 2021-09-11 12:06:22 +08:00
crazywhalecc
432fd92cca update docs 2021-09-11 12:00:48 +08:00
crazywhalecc
907a9cea25 update to build 420 2021-09-11 11:59:02 +08:00
07391810ff update to build 419 2021-09-11 11:07:23 +08:00
5063421364 update docs 2021-09-10 11:28:56 +08:00
2b4d308783 update to build 418 2021-09-10 11:24:32 +08:00
e2f49968b3 Merge remote-tracking branch 'origin/master' 2021-09-01 14:14:13 +08:00
229778ebf9 update to build 417 2021-09-01 14:14:00 +08:00
Jerry Ma
d2c0972c93 Add actions badge [skip ci] 2021-08-31 13:40:29 +08:00
Jerry Ma
d300b6e518 Update main.yml 2021-08-31 13:39:11 +08:00
Jerry Ma
56cb7b2223 Update main.yml 2021-08-30 16:42:47 +08:00
Jerry Ma
2fc42d5d60 Update README.md 2021-08-30 16:40:53 +08:00
Jerry Ma
999e90f709 Update index.md 2021-08-30 16:17:13 +08:00
Jerry Ma
45c6cd7d2a Update main.yml 2021-08-30 16:12:35 +08:00
Jerry Ma
5aa0858021 Update main.yml 2021-08-30 16:05:15 +08:00
Jerry Ma
e981da3932 Update main.yml 2021-08-30 16:00:36 +08:00
Jerry Ma
2685be7306 Create main.yml 2021-08-30 11:27:51 +08:00
Jerry Ma
a13c4628f5 Update README.md 2021-07-15 11:37:35 +08:00
Jerry Ma
b36417454c Update README.md 2021-07-10 15:55:15 +08:00
102 changed files with 2663 additions and 280 deletions

33
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Docs Build
on:
push:
branches:
- master
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v2
- name: Deploy docs to GitHub Pages
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUSTOM_DOMAIN: framework.zhamao.me
CONFIG_FILE: mkdocs.yml
EXTRA_PACKAGES: build-base
- name: Copy deployment to current folder
run: |
cp -r "${GITHUB_WORKSPACE}/site" "./deploy"
- name: Deploy to Zhamao Server
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.ZHAMAO_XIN_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "deploy/"
REMOTE_HOST: ${{ secrets.ZHAMAO_XIN_HOST }}
REMOTE_USER: ${{ secrets.ZHAMAO_XIN_USER }}
TARGET: ${{ secrets.ZHAMAO_XIN_TARGET }}

View File

@@ -7,6 +7,7 @@
[![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE) [![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) [![Latest Stable Version](http://img.shields.io/packagist/v/zhamao/framework.svg)](https://packagist.org/packages/zhamao/framework)
[![Banner](https://img.shields.io/badge/OneBot-v11-success)](https://github.com/howmanybots/onebot) [![Banner](https://img.shields.io/badge/OneBot-v11-success)](https://github.com/howmanybots/onebot)
![Build Actions](https://github.com/zhamao-robot/zhamao-framework/actions/workflows/main.yml/badge.svg)
[![注解数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/AnnotationBase.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=AnnotationBase) [![注解数量](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) [![TODO 数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
@@ -42,19 +43,19 @@ public function index() {
## 开始 ## 开始
如果你是初学者,可以直接使用以下脚本部署 PHP 环境和安装框架的脚手架: 如果你是初学者,可以直接使用以下脚本部署 PHP 环境和安装框架的脚手架:
```bash ```bash
# 新建一个自己喜欢名字的文件夹,运行一键安装脚本 # 新建一个自己喜欢名字的文件夹,运行一键安装脚本 (仅限 x86_64 和 aarch64 平台)
mkdir zhamao-app/ mkdir zhamao-app/
cd zhamao-app/ cd zhamao-app/
bash -c "$(curl -fsSL https://api.zhamao.xin/go.sh)" bash -c "$(curl -fsSL https://api.zhamao.xin/go.sh)"
# 启动 # 启动
runtime/php vendor/bin/start server vendor/bin/start server
``` ```
## 文档v2 版本) ## 文档v2 版本)
查看文档(国内自建):<https://docs-v2.zhamao.xin/> 查看文档(国内自建):<https://framework.zhamao.xin/>
备用链接(国外托管):<https://docs-v2.zhamao.me/> 备用链接(国外托管):<https://framework.zhamao.me/>
自行构建文档:`mkdocs build -d distribute` 自行构建文档:`mkdocs build -d distribute`
@@ -97,7 +98,7 @@ runtime/php vendor/bin/start server
## 关于 ## 关于
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。 框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
作者的炸毛机器人已从2018年初起稳定运行了**年**,并且持续迭代。 作者的炸毛机器人已从2018年初起稳定运行了**年**,并且持续迭代。
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ[627577391](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes))或提交 Issue 进行疑难解答。 欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ[627577391](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes))或提交 Issue 进行疑难解答。

1
bin/phpunit-swoole Executable file → Normal file
View File

@@ -59,6 +59,7 @@ require PHPUNIT_COMPOSER_INSTALL;
$starttime = microtime(true); $starttime = microtime(true);
go(function () { go(function () {
try { try {
require_once __DIR__.'/../test/bootstrap.php';
PHPUnit\TextUI\Command::main(false); PHPUnit\TextUI\Command::main(false);
} catch (Exception $e) { } catch (Exception $e) {
echo $e->getMessage() . PHP_EOL; echo $e->getMessage() . PHP_EOL;

0
bin/start Executable file → Normal file
View File

View File

@@ -35,7 +35,7 @@
"jelix/version": "^2.0", "jelix/version": "^2.0",
"league/climate": "^3.6", "league/climate": "^3.6",
"psy/psysh": "@stable", "psy/psysh": "@stable",
"doctrine/orm": "^2.9" "doctrine/dbal": "^2.13.1"
}, },
"suggest": { "suggest": {
"ext-ctype": "Use C/C++ extension instead of polyfill will be more efficient", "ext-ctype": "Use C/C++ extension instead of polyfill will be more efficient",

View File

@@ -34,10 +34,14 @@ $config['swoole'] = [
//'task_enable_coroutine' => true //'task_enable_coroutine' => true
]; ];
/** 一些框架与Swoole运行时设置的调整 */
/** 一些框架与框架运行时设置的调整 */
$config['runtime'] = [ $config['runtime'] = [
'swoole_coroutine_hook_flags' => SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL), 'swoole_coroutine_hook_flags' => SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL),
'swoole_server_mode' => SWOOLE_PROCESS 'swoole_server_mode' => SWOOLE_PROCESS,
'middleware_error_policy' => 1,
'reload_delay_time' => 800,
'global_middleware_binding' => []
]; ];
/** 轻量字符串缓存,默认开启 */ /** 轻量字符串缓存,默认开启 */
@@ -55,25 +59,10 @@ $config['worker_cache'] = [
'transaction_timeout' => 30000 'transaction_timeout' => 30000
]; ];
/** @deprecated 放弃使用,旧版数据库,请使用 mysql_config 和 doctrine/dbal 搭配使用 */
$config['sql_config'] = [
'sql_host' => '',
'sql_port' => 3306,
'sql_username' => 'name',
'sql_database' => 'db_name',
'sql_password' => '',
'sql_options' => [
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false
],
'sql_no_exception' => false,
'sql_default_fetch_mode' => PDO::FETCH_ASSOC //added in 1.5.6
];
/** MySQL数据库连接信息host留空则启动时不创建sql连接池 */ /** MySQL数据库连接信息host留空则启动时不创建sql连接池 */
$config['mysql_config'] = [ $config['mysql_config'] = [
'host' => '', 'host' => '',
'port' => 33306, 'port' => 3306,
'unix_socket' => null, 'unix_socket' => null,
'username' => 'root', 'username' => 'root',
'password' => '123456', 'password' => '123456',
@@ -133,7 +122,8 @@ $config['static_file_server'] = [
$config['onebot'] = [ $config['onebot'] = [
'status' => true, 'status' => true,
'single_bot_mode' => false, 'single_bot_mode' => false,
'message_level' => 99999 'message_level' => 99,
'message_convert_string' => true
]; ];
/** 一个远程简易终端使用nc直接连接即可但是不建议开放host为0.0.0.0(远程连接) */ /** 一个远程简易终端使用nc直接连接即可但是不建议开放host为0.0.0.0(远程连接) */

View File

@@ -42,6 +42,21 @@ MessageUtil::containsImage("[CQ:image,file=a.jpg,url=http://xxx]"); // true
MessageUtil::containsImage("[CQ:face,id=140] 咦,这是一条带表情的消息"); // false MessageUtil::containsImage("[CQ:face,id=140] 咦,这是一条带表情的消息"); // false
``` ```
### isAtMe()
检查消息中是否含有@bot的消息
定义:`isAtMe($msg, $me_id)`
参数 `$me_id` 为Bot的QQ号。
返回:`bool`true 就是有false 就没有。
```php
MessageUtil::isAtMe("[CQ:at,qq=123456]炸毛你好","123456"); // true
MessageUtil::isAtMe("[CQ:at,qq=123456789]另一个朋友你好","123456"); // false
```
### getImageCQFromLocal() ### getImageCQFromLocal()
通过文件路径获取图片的发送 CQ 码。 通过文件路径获取图片的发送 CQ 码。
@@ -113,3 +128,52 @@ public function onStart() {
( 其实还是很聪明的! ( 其实还是很聪明的!
</chat-box> </chat-box>
### strToArray()
`string` 类型的消息文本转换为 `array` 格式。
定义:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
参数 `$msg` 为带 OB/CQ 码的字符串消息,如 `你好啊,[CQ:at,qq=123]`
参数 `$ignore_space``false` 时,转换的数组内会包含空 `text` 段。
参数 `$trim_text``true` 时,会自动去除 `text` 段消息头尾的换行符和空格。
这个命令转换的数组格式符合 OneBot 11/12 标准,但细节上可能会与不同 OneBot 实现有所差异。
```php
$str = "你好啊,[CQ:at,qq=123]";
$arr = \ZM\Utils\MessageUtil::strToArray($str);
```
转换结果参考如下:
```json
[
{
"type": "text",
"data": {
"text": "你好啊,"
}
},
{
"type": "at",
"data": {
"qq": "123"
}
}
]
```
### arrayToStr()
`array` 格式的消息内容转换为字符串 + CQ 码的形式。
定义:`arrayToStr(array $array)`
```php
// 我们使用上边的 $arr 作为传入值。
$new_str = \ZM\Utils\MessageUtil::arrayToStr($arr);
// 结果:"你好啊,[CQ:at,qq=123]"
```

View File

@@ -0,0 +1,9 @@
# 机器人 APIOneBotV12待发布
!!! tip "提示"
目前由于 OneBot 12 标准还没有定稿,处于草案阶段,故框架暂不更新。
在未来升级到 OneBot 12 标准后,框架会提供转换及兼容措施以及 12 版本的 API 方法。
见 [机器人动作OneBot 11](../robot-api)。

View File

@@ -1,26 +1,29 @@
# 机器人 APIZMRobot # 机器人 APIOneBotV11
ZMRobot 类是封装好的 OneBot 标准的 API 接口调用类,可以在机器人连接后通过连接或者机器人 QQ 号获取对象并调用接口(如发送群消息、获取群列表等操作)。 OneBotV11 类是封装好的 OneBot 标准的 API 接口调用类,可以在机器人连接后通过连接或者机器人 QQ 号获取对象并调用接口(如发送群消息、获取群列表等操作)。
| 属性项 | 属性值 | 备注 | | 属性项 | 属性值 | 备注 |
| -------- | ---------------- | ------------------------------ | | -------- | ------------------ | -------------------------------- |
| 名称 | ZMRobot | | | 名称 | OneBotV11 | |
| 类型 | 实例化类 | `$r = new ZMRobot($conn)` | | 类型 | 实例化类 | `$r = new OneBotV11($conn)` |
| 命名空间 | `ZM\API\ZMRobot` | 使用前先 `use ZM\API\ZMRobot;` | | 命名空间 | `ZM\API\OneBotV11` | 使用前先 `use ZM\API\OneBotV11;` |
| 别名 | `ZM\API\ZMRobot` | 此类目前是 `extends OneBotV11` |
> 你也可以继续使用 2.5 版本之前的别名类 `ZMRobot`,但未来框架将会优先兼容 OneBot V12 版本的协议,可能会造成更新问题,建议切换为 OneBotV11 类。
## 属性 ## 属性
对象属性方法是对 API 的调整,例如是否以 `_async``_rate_limited` 后缀发送 API、设置协程返回还是异步返回结果等。 对象属性方法是对 API 的调整,例如是否以 `_async``_rate_limited` 后缀发送 API、设置协程返回还是异步返回结果等。
### ZMRobot::API_NORMAL ### OneBotV11::API_NORMAL
以默认(无后缀)方式请求 API。 以默认(无后缀)方式请求 API。
### ZMRobot::API_ASYNC ### OneBotV11::API_ASYNC
以后缀 `_async` 方式异步请求 API。 以后缀 `_async` 方式异步请求 API。
### ZMRobot::API_RATE_LIMITED ### OneBotV11::API_RATE_LIMITED
以后缀 `_rate_limited` 方式请求 API。 以后缀 `_rate_limited` 方式请求 API。
@@ -30,7 +33,7 @@ ZMRobot 类是封装好的 OneBot 标准的 API 接口调用类,可以在机
设置后缀。目前支持 `_async``_rate_limited` 设置后缀。目前支持 `_async``_rate_limited`
- **prefix**: `int` `默认:API_NORMAL`,可选 `ZMRobot::API_NORMAL``ZMRobot::API_ASYNC``ZMRobot::API_RATE_LIMITED` - **prefix**: `int` `默认:API_NORMAL`,可选 `OneBotV11::API_NORMAL``OneBotV11::API_ASYNC``OneBotV11::API_RATE_LIMITED`
设置后缀后,请求的 API 会发生变化。例如发送私聊消息:`sendPrivateMsg()`,请求的 API 为 `send_private_msg_async`,详见 [OneBot 文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/README.md)。 设置后缀后,请求的 API 会发生变化。例如发送私聊消息:`sendPrivateMsg()`,请求的 API 为 `send_private_msg_async`,详见 [OneBot 文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/README.md)。
@@ -43,22 +46,22 @@ ZMRobot 类是封装好的 OneBot 标准的 API 接口调用类,可以在机
获取当前对象的机器人 QQ 或 OneBot 实例的 ID。 获取当前对象的机器人 QQ 或 OneBot 实例的 ID。
```php ```php
$bot = ZMRobot::get(123456); $bot = OneBotV11::get(123456);
echo $bot->getSelfId(); //123456 echo $bot->getSelfId(); //123456
``` ```
### ZMRobot::get() ### OneBotV11::get()
静态方法,用来通过机器人 QQ 或 OneBot 实例的 ID 获取 ZMRobot 对象。 静态方法,用来通过机器人 QQ 或 OneBot 实例的 ID 获取 OneBotV11对象。
参数:`$robot_id`,必填。 参数:`$robot_id`,必填。
```php ```php
$r = ZMRobot::get(123456); $r = OneBotV11::get(123456);
$r->sendPrivateMsg(55555, "hello"); $r->sendPrivateMsg(55555, "hello");
``` ```
### ZMRobot::getRandom() ### OneBotV11::getRandom()
静态方法,随机获取一个连接到框架的机器人(多个机器人实例连接到框架时适用)。 静态方法,随机获取一个连接到框架的机器人(多个机器人实例连接到框架时适用)。
@@ -66,21 +69,21 @@ $r->sendPrivateMsg(55555, "hello");
```php ```php
try { try {
$bot = ZMRobot::getRandom(); $bot = OneBotV11::getRandom();
$bot->sendPrivateMsg(55555, "foo"); $bot->sendPrivateMsg(55555, "foo");
} catch (\ZM\Exception\RobotNotFoundException $e) { } catch (\ZM\Exception\RobotNotFoundException $e) {
echo "还没有机器人连接到框架!\n"; echo "还没有机器人连接到框架!\n";
} }
``` ```
### ZMRobot::getAllRobot() ### OneBotV11::getAllRobot()
获取所有连接到框架的机器人的 ZMRobot 对象。 获取所有连接到框架的机器人的 OneBotV11 对象。
返回值:`ZMRobot[]` 返回值:`OneBotV11[]`
```php ```php
$all = ZMRobot::getAllRobot(); $all = OneBotV11::getAllRobot();
foreach($all as $v) { foreach($all as $v) {
$v->sendPrivateMsg(55555, "机器人轮流给一个人发消息啦!"); $v->sendPrivateMsg(55555, "机器人轮流给一个人发消息啦!");
} }
@@ -95,7 +98,7 @@ foreach($all as $v) {
```php ```php
//从上下文获取 Websocket 连接对象 //从上下文获取 Websocket 连接对象
$conn = ctx()->getConnection(); $conn = ctx()->getConnection();
$bot = new ZMRobot($conn); $bot = new OneBotV11($conn);
``` ```
## 返回结果处理 ## 返回结果处理
@@ -103,7 +106,7 @@ $bot = new ZMRobot($conn);
因为框架的机器人是兼容 OneBot 标准的(原 CQHTTP所以每次接收发送 API 请求的结果都是大体一样的结构。我们以 `sendPrivateMsg()` 为例,因为发送出去的每一条消息都会在 OneBot 实例(如 CQHTTP 插件、go-cqhttp 等)中对应一个消息 ID以供我们核查消息和后续撤回等操作需要。 因为框架的机器人是兼容 OneBot 标准的(原 CQHTTP所以每次接收发送 API 请求的结果都是大体一样的结构。我们以 `sendPrivateMsg()` 为例,因为发送出去的每一条消息都会在 OneBot 实例(如 CQHTTP 插件、go-cqhttp 等)中对应一个消息 ID以供我们核查消息和后续撤回等操作需要。
```php ```php
$bot = ZMRobot::get("123456"); // 机器人QQ号 $bot = OneBotV11::get("123456"); // 机器人QQ号
$obj = $bot->sendGroupMsg("234567", "你好"); $obj = $bot->sendGroupMsg("234567", "你好");
echo json_encode($obj, 128|256); echo json_encode($obj, 128|256);
``` ```
@@ -163,7 +166,7 @@ vardump($result["retcode"]); //如果成功撤回,输出 int(0)
=== "代码" === "代码"
```php ```php
$bot = ZMRobot::get(123456); // 123456是你的机器人QQ $bot = OneBotV11::get(123456); // 123456是你的机器人QQ
$bot->sendPrivateMsg("627577391", "你好啊!你好你好!"); $bot->sendPrivateMsg("627577391", "你好啊!你好你好!");
``` ```

View File

@@ -124,7 +124,7 @@ public function ping() {
## getRobot() - 获取机器人 API 对象 ## getRobot() - 获取机器人 API 对象
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](bot/robot-api.md)。 返回当前上下文关联的机器人 API 调用对象 [ZMRobot](../bot/robot-api.md)。
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。 可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。

View File

@@ -0,0 +1,104 @@
# 事件跟踪器及调试
众所周知炸毛框架中的事件由内置的事件分发器EventDispatcher负责分发但调试事件分发在之前的版本比较困难例如不能获取到事件如何被调用以及事件如何被捕获。
EventTracer 的作用是记录事件的调用顺序,以便于调试。
命名空间使用指南:`use ZM\Event\EventTracer;`
## EventTracer::getCurrentEvent() - 获取当前注解事件对象
```php
/**
* @OnStart()
*/
public function onStart() {
zm_dump(EventTracer::getCurrentEvent());
}
/*
^ ZM\Annotation\Swoole\OnStart^ {#192
+worker_id: 0
+method: "onStart"
+class: "Module\Example\Hello"
}
*/
```
这里这个方法必须在注解事件内执行,如果在注解事件外执行,将会返回 `null`
## EventTracer::getCurrentEventMiddlewares() - 获取当前注解事件的中间件们
```php
/**
* @OnStart()
* @Middleware("timer")
*/
public function onStart() {
zm_dump(EventTracer::getCurrentEventMiddlewares());
}
/*
^ array:1 [
0 => ZM\Annotation\Http\Middleware^ {#194
+middleware: "timer"
+params: []
+method: "onStart"
+class: "Module\Example\Hello"
}
]
*/
```
返回值为当前注解事件的中间件们,如果没有注解中间件,返回 `[]`
## EventTracer::getEventTraceList() - 获取注解事件的列表
此处返回的是 `getCurrentEvent()` 相同的对象,但是返回的是一个数组,数组中的元素是注解事件。
```php
/**
* 一个简单随机数的功能demo
* 问法1随机数 1 20
* 问法2从1到20的随机数
* @CQCommand("随机数")
* @Middleware("timer")
* @CQCommand(pattern="*从*到*的随机数")
* @return string
*/
public function randNum() {
// 此处为随机数代码
zm_dump(EventTracer::getEventTraceList());
return "随机数:" . rand(1, 20);
}
/*
^ array:2 [
0 => ZM\Annotation\CQ\CQCommand^ {#193
+match: ""
+pattern: "*从*到*的随机数"
+regex: ""
+start_with: ""
+end_with: ""
+keyword: ""
+alias: []
+message_type: ""
+user_id: 0
+group_id: 0
+discuss_id: 0
+level: 20
+method: "randNum"
+class: "Module\Example\Hello"
}
1 => ZM\Annotation\Swoole\OnMessageEvent^ {#165
+connect_type: "default"
+rule: "connectIsQQ()"
+level: 99
+method: "handleByEvent"
+class: "ZM\Module\QQBot"
}
]
*/
```
## EventDispatcher::enableEventTrace() - 启用事件跟踪器
还没写完,不着急。

View File

@@ -14,7 +14,6 @@ src/
   │   └── zm.json    │   └── zm.json
   └── Middleware/    └── Middleware/
   └── TimerMiddleware.php    └── TimerMiddleware.php
``` ```
我们在 Example 目录下创建一个 `zm.json` 的文件,编写配置,即代表 `src/Module/Example/` 文件夹及里面的用户模块源码为一个模块包,也就可以被框架识别并打包处理。 我们在 Example 目录下创建一个 `zm.json` 的文件,编写配置,即代表 `src/Module/Example/` 文件夹及里面的用户模块源码为一个模块包,也就可以被框架识别并打包处理。
@@ -158,4 +157,86 @@ src/
} }
``` ```
在打包时框架会自动添加这些文件到 phar 插件包内,到解包时,会自动将这些文件释放到对应框架的 `zm_data` 目录下。 在打包时框架会自动添加这些文件到 phar 插件包内,到解包时,会自动将这些文件释放到对应框架的 `zm_data` 目录下。
## 打包模块命令
编写配置文件 `zm.json` 后,就可以被框架正常识别为模块形式,你也可以使用对无需打包的模块进行配置以进行分类管理。
### module:list
使用 list 命令可以列出炸毛框架检测到配置文件或打包好的模块。
```
$ ./zhamao module:list
[foo]
类型: source
版本: 1.0.0
描述: 示例模块打包文件
目录: src/Module/Example
没有发现已打包且装载的模块!
```
其中 `[ ]` 内为识别出来的模块名称,由上方用户编写的 `zm.json` 定义,类型为 `source` 是源码形式,也就是指定了 `zm.json` 形式的模块,目录为模块所在子目录。
我们假设打包上方定义的 `foo` 模块,使用下方命令 `module:pack` 即可。
### module:pack
使用 pack 命令可以将配置好的模块打包为 `xxx.phar` 文件并转移或发布给他人。
我们假设打包模块脚手架的默认模块 `src/Module/Example` 下面的模块源码和附带一个 `zm_data` 目录下的文件(我们就随便打包一下 Swoole 的输出日志吧)。`zm.json` 文件内容如下:
```json
{
"name": "foo",
"description": "示例模块打包文件",
"version": "1.0.0",
"allow-hotload": true,
"zm-data-store": [
"crash/swoole_error.log"
]
}
```
然后输入命令:
```
$ ./zhamao module:pack foo
[15:07:11] [I] 模块输出文件:/root/zhamao-framework/zm_data/output/foo_1.0.0.phar
[15:07:11] [S] 打包完成!
```
如果提示文件夹不存在,请先手动创建文件夹:`mkdir /path/to/your/zm_data/output`
在打包后,你将获得一个 `foo_1.0.0.phar` 的文件。
> 如果你没有在 `zm.json` 中指定 `version`,那么输出的 phar 文件是不会带版本号的。
打包后的 phar 内将包含:
- Hello.php
- zm.json
- crash/swoole_error.log
- 必要的框架热加载以及解包需要的配置信息
## 打包命令
```bash
# ./zhamao 和原先的 vendor/bin/start 是完全一致的
./zhamao module:pack <module-name>
```
例如,打包上面的名叫 foo 的模块:`./zhamao module:pack foo`。
打包命令执行后,将会在 `zm_data` 下的 `output` 目录输出一个 phar 文件。如果你指定了 `version` 参数,那么文件名将会是 `${name}_${version}.phar`,如果没有指定版本,那么只会有 `${name}.phar`,同时如果文件已经存在,将覆盖写入。
## 查看模块信息命令
```bash
./zhamao module:list
```
通过此命令可以查看模块相关的信息,如未打包但已配置的模块信息等。

View File

@@ -0,0 +1,84 @@
# 模块解包
从 2.5 版本起,炸毛框架的模块源码支持了打包和分发,分发后必不可少的一步就是将其解包。
解包过程大致为:
1. 检查模块的配置文件是否正常。
2. 检查模块的依赖问题,如果有依赖但未安装,则抛出异常。
3. 检查 LightCache 轻量缓存是否需要写入。
4. 检查 `zm_data` 是否有需要存入的数据。
5. 合并 `composer.json` 文件。
6. 拷贝 `zm_data` 相关的文件。
7. 写入 LightCache 相关数据。
8. 提示用户手动合并 `global.php` 全局配置文件。
9. 拷贝模块 PHP 源文件。
## 解包命令
```bash
./zhamao module:unpack <module-name>
```
首先将待解包的 phar 文件放入 `zm_data` 目录下的 `modules` 文件夹(如果不存在需要手动创建),如果你手动修改过 `global.php` 下面的 `module_loader.load_path` 项,需要放入对应的目录。
放入后,结构如下:
```
zm_data/
zm_data/modules/
zm_data/modules/foo.phar
```
接下来需要知道模块的名称。当然一般情况下phar 的名称可以获取到模块的实际名称,如 `foo`,但最好用 `./zhamao module:list` 列出模块的信息来获取真实的模块名称。
```
./zhamao module:list
# 下面是输出
[foo]
类型: 模块包(phar)
位置: zm_data/modules/我是假的名字.phar
```
解包过程十分简单,只需要执行一次命令即可。
```
./zhamao module:unpack foo
# 下面是输出
[10:05:40] [I] Releasing source file: src/Module/Example/Hello.php
[10:05:40] [I] Releasing source file: src/Module/Example/zm.json
[10:05:40] [S] 解压完成!
```
### 命令参数
在解包时会遇到各种复杂的情况,如源码文件已存在、数据已存在、依赖问题等,通过增加参数可以控制解包时的行为。
#### --overwrite-light-cache
含义:覆盖现有的 LightCache 键值(如果存在的话)。
#### --overwrite-zm-data
含义:覆盖现有的 `zm_data` 下的文件(如果存在的话)。
#### --overwrite-source
含义:覆盖现有的 PHP 模块源文件(如果存在的话)。
#### --ignore-depends
含义:解包时忽略检查依赖。
### 常见问题
如果你解包的模块包要求修改 `global.php`,则会出现类似这样的提示:
```
# ./zhamao module:unpack foo
[14:47:39] [W] 模块作者要求用户手动修改 global.php 配置文件中的项目:
[14:47:39] [W] *请把全局配置文件的light_cache项目中max_strlen选项调整为至少65536
请输入修改模式y(使用vim修改)/e(自行使用其他编辑器修改后确认)/N(默认暂不修改)[y/e/N]
```
一般这种情况,根据第二条提示(第二条提示为打包时填入的 `global-config-override`)。如果输入 y则会自动执行命令 `vim config/global.php`,如果输入的是 e则会等待你手动修改完成文件最后按回车完成修改。默认情况直接回车的话会跳过此步骤如果模块要求了修改但跳过修改安装后可能会有功能缺失等问题。

View File

@@ -125,9 +125,9 @@ dump(LightCache::getExpire("test")); // 返回 10
```php ```php
$s = LightCache::set("test", "hello", 20); //假设这条代码执行时时间戳是 1616838482 $s = LightCache::set("test", "hello", 20); //假设这条代码执行时时间戳是 1616838482
zm_sleep(10); zm_sleep(10);
dump(LightCache::getExpire("test")); // 返回 1616838502 dump(LightCache::getExpireTS("test")); // 返回 1616838502
zm_sleep(10); zm_sleep(10);
dump(LightCache::getExpire("test")); // 返回 null dump(LightCache::getExpireTS("test")); // 返回 null
``` ```
### LightCache::getMemoryUsage() ### LightCache::getMemoryUsage()

View File

@@ -1,4 +1,8 @@
# MySQL 数据库 # MySQL 数据库(旧版组件)
!!! warning "注意"
此 MySQL 组件为旧版 MySQL 查询器组件,为了统一和提升对未来独立组件的兼容性,现转变为使用 `doctrine/dbal``doctrine/orm` 库来实现查询器,请转到 [MySQL 查询器]()。
## 配置 ## 配置

View File

@@ -0,0 +1,184 @@
# 执行 SQL 语句
在一开始,无论你做什么数据库操作,均需要获取一个 `\ZM\MySQL\MySQLWrapper` 作为你的操作对象。
```php
/** @var \ZM\MySQL\MySQLWrapper $wrapper */
$wrapper = \ZM\MySQL\MySQLManager::getWrapper();
```
!!! tip "提示"
这部分内容部分直接取自 [DBAL - Data Retrieval And Manipulation](https://www.doctrine-project.org/projects/doctrine-dbal/en/2.13/reference/data-retrieval-and-manipulation.html) 原文并直接翻译,如有实际不同请提交 Issue 反馈。
## 执行预处理 SQL 语句
预处理查询很巧妙地解决了 SQL 注入问题,并且可以方便地绑定参数进行查询。
预处理一般是指使用 `?` 占位符或 `:xxx` 命名标签进行参数留空,先处理 SQL 语句再填入数据。
一般 `?` 具有前后位置性,例如如下的查询:
```php
$sql = "SELECT * FROM users WHERE id = ? AND username = ?";
$stmt = $wrapper->getConnection()->prepare($sql);
$stmt->bindValue(1, "1");
$stmt->bindValue(2, "jack");
$resultSet = $stmt->executeQuery();
```
其中 `$resultSet``Statement` 方法相似,此处的对象可能是 [数据库语句对象](../mysql-statement) 或 数据库结果对象(结果对象与语句对象的 `fetchXXX()` 部分一致)。
这里也可以使用命名标签,使用标签可以给相同参数处使用同一个标签:
```php
$sql = "SELECT * FROM users WHERE gender = :name OR username = :name";
$stmt = $wrapper->getConnection()->prepare($sql);
$stmt->bindValue("name", "jack");
$resultSet = $stmt->executeQuery();
```
## 执行常规语句
执行常规语句为 `statement` 方式执行,此方法执行后只返回影响的行数,而不返回结果,适用于 `UPDATE` 等语句。
```php
<?php
$count = $wrapper->executeStatement('UPDATE users SET username = ? WHERE id = ?', array('jwage', 1));
echo $count; // 1
```
## 执行查询语句
为给定的 SQL 创建一个准备好的语句并将参数传递给 executeQuery 方法,然后返回结果集。此方法为上述的「预处理查询语句」的简化版,可直接在第二个参数使用 array 插入绑定参数执行。
```php
$resultSet = $wrapper->executeQuery('SELECT * FROM user WHERE username = ?', array('jack'));
$user = $resultSet->fetchAssociative();
/* $user 值
array(
0 => array(
'id' => 1,
'username' => 'jack',
'gender' => 'man',
'update_time' => '2021-10-12'
)
)
*/
```
### fetchAllAssociative()
执行查询并将所有结果返回一个数组中。
因此,上面的查询语句还可以直接被简化为一次方法调用:
```php
$resultSet = $wrapper->fetchAllAssociative('SELECT * FROM user WHERE username = ?', array('jack'));
// 结果同 executeQuery()->fetchAllAssociative() 中 $user 的值。
```
### fetchAllKeyValue()
执行查询并将前两列分别作为键和值提取到关联数组中。
```php
$resultSet = $wrapper->fetchAllKeyValue('SELECT username, gender FROM user WHERE username = ?', array('jack'));
/* $resultSet 值
array(
'jack' => 'man'
)
*/
```
### fetchAllAssociativeIndexed()
执行查询并将数据作为关联数组获取,其中键代表第一列,值是其余列及其值的关联数组。
```php
$users = $wrapper->fetchAllAssociativeIndexed('SELECT id, username, gender FROM users');
/*
array(
1 => array(
'username' => 'jack',
'gender' => 'man',
'update_time' => '2021-10-12'
)
)
*/
```
### fetchNumeric()
查询并返回第一行数据,形式以数字索引方式返回每一列。
```php
$user = $wrapper->fetchNumeric('SELECT * FROM users WHERE username = ?', array('jack'));
/*
array(
0 => 'jwage',
1 => 'man',
2 => '2021-10-12'
)
*/
```
### fetchOne()
仅返回查询结果的第一行第一列的值。
```php
$username = $wrapper->fetchOne('SELECT username FROM users WHERE id = ?', array(1));
echo $username; // jack
```
### fetchAssociative()
返回结果内第一行的关联数组形式的数据。
```php
$users = $wrapper->fetchAssociative('SELECT * FROM users');
/*
array(
'id' => 1,
'username' => 'jack',
'gender' => 'man',
'update_time' => '2021-10-12'
)
*/
```
### delete()
删除查询操作,第一个参数为表名,第二个参数为 `['列名' => '列值']`
```php
<?php
$wrapper->delete('users', array('username' => 'jack'));
// 等同于执行DELETE FROM user WHERE username = ? ,参数列表为('jack')
```
### insert()
插入数据库一行,第一个参数为表名,第二个参数为对应数据。
```php
$wrapper->insert('users', array('id' => 0, 'username' => 'jwage', 'gender' => 'woman', 'update_time' => '2021-10-17'));
// INSERT INTO user (id, username, gender, update_time) VALUES (?,?,?,?) (0,jwage,woman,2021-10-17)
```
### update()
更新数据库,使用给定数据更新匹配键值标识符的所有行。
```php
<?php
$wrapper->update('user', array('username' => 'jwage'), array('id' => 1));
// UPDATE user (username) VALUES (?) WHERE id = ? (jwage, 1)
```

View File

@@ -0,0 +1,7 @@
# 配置
炸毛框架的数据库组件支持原生 SQL、查询构造器去掉了复杂的对象模型关联同时默认为数据库连接池使开发变得简单。
数据库的配置位于 `config/global.php` 文件的 `mysql_config` 段,见 [全局配置](../../../../guide/basic-config#mysql_config)。
如果 `mysql_config.host` 字段为空,则不创建数据库连接池,填写后将创建,且默认保持长连接。

View File

@@ -0,0 +1 @@
你好啊,这里是 Statement。

View File

@@ -0,0 +1,14 @@
# MySQL 数据库简介
炸毛框架的数据库组件对接了 MySQL 连接池,在使用过程中无需配置即可实现 MySQL 查询,同时拥有高并发。
目前 2.5 版本后炸毛框架底层采用了 `doctrine/dbal` 组件,可以方便地构建 SQL 语句。
本章大体查询内容均以下表 `users` 为基础:
| id | username | gender | update_time |
| -- | -------- | ------ | ----------- |
| 1 | jack | man | 2021-10-12 |
| 2 | rose | woman | 2021-10-11 |
#

View File

@@ -2,6 +2,10 @@
前面讲到 LightCache 轻量缓存在特定的情况下为了保证数据不被多进程的因素导致丢失或覆盖,在高并发情况下修改数据需要加锁,所以炸毛框架内置了 SpinLock 自旋锁。 前面讲到 LightCache 轻量缓存在特定的情况下为了保证数据不被多进程的因素导致丢失或覆盖,在高并发情况下修改数据需要加锁,所以炸毛框架内置了 SpinLock 自旋锁。
!!! tip "提示"
框架单进程运行的模式下不需要任何自旋锁。
## 配置 ## 配置
自旋锁使用无需配置,和 LightCache 同源。 自旋锁使用无需配置,和 LightCache 同源。

View File

@@ -165,3 +165,31 @@ public function onThrowing(?Exception $e) {
`ctx()` 为获取当前协程空间绑定的 `request``response` 对象。 `ctx()` 为获取当前协程空间绑定的 `request``response` 对象。
## 中间件加载错误处理策略
中间件在某些情况下可能会产生普通 PHP 异常以外的异常,不能被框架的正常错误流程捕获,所以这里额外说明了中间件异常处理的几种策略。
中间件异常处理策略可以在 2.5.2 版本之后通过 `global.php` 中的 `runtime``middleware_error_policy` 设置。
- **0**: 无论被运行事件中间件是否存在,都不抛出异常,继续执行事件。
- **1**: 在框架启动时如果某事件被注解了一个不存在的中间件,则不抛出异常,在执行期间才检测是否存在此中间件,并抛出异常。
- **2**: 严格的中间件检查,在框架启动时就检测所有被注解了中间件的注解事件。
假设我们有一个路由注解 `@RequestMapping("/test")`,同时注解了一个不存在的中间件,如下:
```php
/**
* @RequestMapping("/test")
* @Middleware("foo")
*/
public function testRoute() {
return "I am testing middleware";
}
```
配置项为 0此中间件类不存在的话则会报告 warning并直接执行此函数。
配置项为 1在访问此路由执行此函数时会抛出异常中断此次事件。
配置项为 2在框架启动时抛出致命异常。

View File

@@ -20,9 +20,10 @@
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` | | `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
| `config_dir` | 存放 saveToJson() 方法保存的数据的目录 | `zm_data` 下的 `config/` | | `config_dir` | 存放 saveToJson() 方法保存的数据的目录 | `zm_data` 下的 `config/` |
| `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` | | `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` |
| `runtime` | 一些框架运行时调整的设置 | 见子表 `runtime` |
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` | | `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` | | `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` | | `mysql_config` | MySQL 数据库连接信息 | 见子表 `mysql_config` |
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` | | `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
| `access_token` | OneBot 客户端连接约定的token留空则无相关设置见 [组件 - Access Token 验证](component/access-token) | 空 | | `access_token` | OneBot 客户端连接约定的token留空则无相关设置见 [组件 - Access Token 验证](component/access-token) | 空 |
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 | | `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
@@ -34,6 +35,7 @@
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表,在 Swoole 服务器启动前就被加载 | 空 | | `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表,在 Swoole 服务器启动前就被加载 | 空 |
| `onebot` | OneBot 协议相关配置 | 见子表 `onebot` | | `onebot` | OneBot 协议相关配置 | 见子表 `onebot` |
| `remote_terminal` | 远程终端相关配置 | 见子表 `remote_terminal` | | `remote_terminal` | 远程终端相关配置 | 见子表 `remote_terminal` |
| `module_loader` | 模块/插件 加载相关配置 | 见子表 `module_loader` |
### 子表 **swoole** ### 子表 **swoole**
@@ -47,6 +49,16 @@
| `task_worker_num` | TaskWorker 工作进程数 | 默认不开启(此参数被注释) | | `task_worker_num` | TaskWorker 工作进程数 | 默认不开启(此参数被注释) |
| `task_enable_coroutine` | TaskWorker 工作进程启用协程 | 默认不开启(此参数被注释)或 `bool` | | `task_enable_coroutine` | TaskWorker 工作进程启用协程 | 默认不开启(此参数被注释)或 `bool` |
### 子表 runtime
| 配置名称 | 说明 | 默认值 |
| ----------------------------- | ------------------------------------------------------------ | --------------------------------------- |
| `swoole_coroutine_hook_flags` | Swoole 启动时一键协程化 Hook 的 Flag 值,详见 [一键协程化](http://wiki.swoole.com/#/runtime?id=%e5%87%bd%e6%95%b0%e5%8e%9f%e5%9e%8b) | `SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL)` |
| `swoole_server_mode` | Swoole Server 启动的进程模式,有 `SWOOLE_PROCESS``SWOOLE_BASE` 两种,见 [启动方式](http://wiki.swoole.com/#/learn?id=swoole_process) | `SWOOLE_PROCESS` |
| `middleware_error_policy` | 中间件错误处理策略,见 [中间件 - 错误处理策略](../../event/middleware/#_6) | 1 |
| `reload_delay_time` | 框架 reload 重载命令接收后延迟的时间毫秒0 为不等待) | 800 |
| `global_middleware_binding` | 给注解事件绑定全局中间件,见 [中间件 - 全局中间件](../../event/middleware/#_6) | `[]` |
### 子表 **light_cache** ### 子表 **light_cache**
| 配置名称 | 说明 | 默认值 | | 配置名称 | 说明 | 默认值 |
@@ -63,17 +75,18 @@
| -------- | --------------------------- | ------ | | -------- | --------------------------- | ------ |
| `worker` | 跨进程缓存的存储工作进程 id | 0 | | `worker` | 跨进程缓存的存储工作进程 id | 0 |
### 子表 **sql_config** ### 子表 **mysql_config**
| 配置名称 | 说明 | 默认值 | | 配置名称 | 说明 | 默认值 |
| ------------------------ | ------------------------------ | ------------------------------------------------------------ | | ------------------------ | ------------------------------ | ------------------------------------------------------------ |
| `sql_host` | 数据库地址(留空则不使用数据库) | 空 | | `host` | 数据库地址(留空则不使用数据库) | 空 |
| `sql_port` | 数据库端口 | 3306 | | `port` | 数据库端口 | 3306 |
| `sql_username` | 连接数据库的用户名 | | | `username` | 连接数据库的用户名 | |
| `sql_database` | 要连接的数据库名 | | | `dbname` | 要连接的数据库名 | |
| `sql_password` | 数据库连接密码 | | | `password` | 数据库连接密码 | |
| `sql_options` | PDO 数据库的 options 参数 | `[PDO::ATTR_STRINGIFY_FETCHES => false,PDO::ATTR_EMULATE_PREPARES => false]` | | `options` | PDO 数据库的 options 参数 | `[PDO::ATTR_STRINGIFY_FETCHES => false,PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]` |
| `sql_default_fetch_mode` | PDO 的 fetch 模式 | `PDO::FETCH_ASSOC` | | `pool_size` | MySQL 连接池大小 | 64 |
| `charset` | MySQL 字符集 | `utf8mb4` |
### 子表 **redis_config** ### 子表 **redis_config**
@@ -95,11 +108,12 @@
### 子表 onebot ### 子表 onebot
| 配置名称 | 说明 | 默认值 | | 配置名称 | 说明 | 默认值 |
| ----------------- | ------------------------------------------------------------ | ------ | | ------------------------ | ------------------------------------------------------------ | ------ |
| `status` | 是否开启 OneBot 标准机器人解析功能 | true | | `status` | 是否开启 OneBot 标准机器人解析功能 | true |
| `single_bot_mode` | 是否开启单机器人模式 | false | | `single_bot_mode` | 是否开启单机器人模式 | false |
| `message_level` | 机器人的 WebSocket 事件在 Swoole 原生事件 `@OnMessageEvent` 中的等级(越高说明越被优先处理) | 99999 | | `message_level` | 机器人的 WebSocket 事件在 Swoole 原生事件 `@OnMessageEvent` 中的等级(越高说明越被优先处理) | 99 |
| `message_convert_string` | 是否将数组格式的消息转换为字符串以保证与旧版本的兼容性 | true |
### 子表 remote_terminal ### 子表 remote_terminal
@@ -110,6 +124,13 @@
| `port` | 远程终端监听的 TCP 端口 | 20002 | | `port` | 远程终端监听的 TCP 端口 | 20002 |
| `token` | 远程终端连接的令牌(如果为空("")则不验证) | "" | | `token` | 远程终端连接的令牌(如果为空("")则不验证) | "" |
### 子表 module_loader
| 配置名称 | 说明 | 默认值 |
| -------- | ------------------------------------------------------------ | ----------- |
| `enable_hotload` | 是否开启热加载模块包的功能 | false |
| `load_path` | 模块包加载的目录地址 | `zm_data` 下的 `modules` |
## 多环境下的配置文件 ## 多环境下的配置文件
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境development / staging / production来编写配置文件。 炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境development / staging / production来编写配置文件。

View File

@@ -69,5 +69,6 @@
| E00067 | 模块解包合并 `composer.json` 时没有找到项目原文件 | 检查项目的工作目录下是否有 `composer.json` 文件存在。 | | E00067 | 模块解包合并 `composer.json` 时没有找到项目原文件 | 检查项目的工作目录下是否有 `composer.json` 文件存在。 |
| E00068 | 模块解包时无法正常拷贝文件 | 检查文件夹是否正常可以创建和写入。 | | E00068 | 模块解包时无法正常拷贝文件 | 检查文件夹是否正常可以创建和写入。 |
| E00069 | 框架不能启动两个 ConsoleApplication 实例 | 不要重复使用 `new ConsoleApplication()`。 | | E00069 | 框架不能启动两个 ConsoleApplication 实例 | 不要重复使用 `new ConsoleApplication()`。 |
| E00070 | 框架找不到 `zm_data` 目录 | 检查配置中指定的 `zm_data` 目录是否存在。 |
| E99999 | 未知错误 | | | E99999 | 未知错误 | |

37
docs/guide/upgrade.md Normal file
View File

@@ -0,0 +1,37 @@
# 升级指南
因为框架在随着需求以及 Bug 在不断更新,所以在未来框架会发布新版本。为了方便从旧版本安装并使用框架的开发者无损更新到新版本,这里提供了升级版本上需要注意的事项。
## 版本约定
炸毛框架的版本号一般情况均按照 [Semantic Versioning 2.0.0](https://semver.org/) 标准进行滚动发行,规则简述如下:
假设版本号为 x.y.z
- `x` 为大版本,一般只有在发生完全无法兼容的更新时增加,需要开发者最重视。
- `y` 为小发行版本,默认情况下会新增组件功能,但会尽可能兼容旧版本,存在不兼容情况极少。
- `z` 为补丁版本,在不进行任何大功能更新情况下提供 Bug 的修复,完全兼容前版本。
例如,炸毛框架的 `2.4.2` 版本,在 `2.5.0` 发行后,框架提供了大量新组件,但是对旧版本的配置和组件完全兼容,无任何额外的说明,则可以直接升级。
## 升级方法
根据安装方法不同,升级的方法也不同。
框架安装方式有多种,但主要分为三类:
- Composer 加载库的方式
- 框架源码模式
- Phar 打包模式
在 Composer 加载库的方式下,一般是指使用命令 `composer require zhamao/framework``composer create-project zhamao/framework-starter` 的方式安装框架,框架的核心文件都在 `vendor` 目录下。
此方式安装的框架升级最方便,直接执行命令 `composer update` 即可。
框架源码模式安装一般为直接使用 `git clone` 框架本体的 GitHub 仓库或下载 master 分支安装,这种情况不可升级版本(或使用 `git pull` 拉取)。
Phar 打包模式更新则必须重新自行打包新版本,例如从 Composer 加载库方式打包的框架,则需在原目录使用 `composer update` 后再次打包一个新版本。
## 升级提示
如果在升级过程中遇到了提示,则可能是需要升级某些配置文件需要手动进行合并更新。如果提示了更新,建议到 `vendor/zhamao/framework/config/global.php` 框架的最新库内配置文件与 `config/global.php` 文件进行对比。

View File

@@ -3,14 +3,12 @@
> 本文档为炸毛框架 v2 版本,如需查看 v1 版本,[点我](https://docs-v1.zhamao.xin/)。 > 本文档为炸毛框架 v2 版本,如需查看 v1 版本,[点我](https://docs-v1.zhamao.xin/)。
!!! tip "提示" !!! tip "提示"
编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request以此来贡献此文档 编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request以此来贡献此文档
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 标准的机器人对接),包含 WebSocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。 炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 标准的机器人对接),包含 WebSocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
框架主要用途为 HTTP/WS 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。 框架主要用途为 HTTP/WebSocket 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
在 HTTP 和 WebSocket 服务器上PHP 的扩展 Swoole 提供了高性能的支持,使其效率可媲美 nginx 静态网页处理的效率。
此外QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快。 此外QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快。
@@ -36,7 +34,7 @@ public function index() {
首先,你需要了解你需要知道哪些事情才能开始着手使用框架: 首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
1. Linux 命令行(会跑 Linux 程序) 1. Linux 命令行(会跑 Linux 程序)
2. php 7.2+ 开发环境(项目会持续支持最新的 PHP 版本) 2. php >=7.2 开发环境(项目会持续支持最新的 PHP 版本)
4. OneBot 机器人聊天接口标准 4. OneBot 机器人聊天接口标准
需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。 需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。

View File

@@ -0,0 +1,97 @@
# 更新日志master 分支 commit
此文档将显示非发布版的提交版本相关更新文档,可能与发布版的更新日志有重合,在此仅作更新记录。
同时此处将只使用 build 版本号进行区分。
## build 428 (2021-11-16)
- 修复 `ctx()->waitMessage()` 在 array 消息模式下无法正确返回消息字符串的问题
- 新增 `ctx()->getArrayMessage()``ctx()->getStringMessage()` 两个方法
- 修复注解事件 `CQCommand``CQMessage` 在 array 消息模式下无法正确解析的 Bug
## build 427 (2021-11-16)
- 新增全局中间件,可在全局配置文件中设置
- 修复部分 Typo
- 新增指令 `server:status``server:reload``server:stop` 可用在新开终端中查看框架运行状态、重启和退出
- 新增支持 `array` 格式的消息
- 上下文 Context 对象新增 `getOriginMessage()` 用于获取原消息,`getMessage()` 如果在设置了转换后,将默认转换消息为字符串格式保持与旧模块兼容
- OneBot API 新增全局过滤器,可用作 Action 过滤重写等操作
- 配置文件新增 `runtime.reload_delay_time`,用于可配置重载 Worker 等待的时间(毫秒)
- 配置文件新增 `runtime.global_middleware_binding`,用于配置全局中间件
- 配置文件新增 `onebot.message_convert_string`,用于配置是否转换数组格式为字符串,保证与前版本的兼容性(默认为 true
- MessageUtil 消息工具类新增方法:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
- MessageUtil 消息工具类新增方法:`arrayToStr(array $array)`
- 新增框架启动多次监测功能,无法使用同一个框架项目同时启动两个框架
## build 426 (2021-11-10)
- 修复 CQ 码的解析函数 Bug#52
## build 425 (2021-11-3)
- 删除未实际应用功能的配置参数
- 修复 reload 时会断开 WebSocket 连接且导致进程崩溃的 Bug
## build 424 (2021-11-2)
- 新增 InstantModule 类、ZMServer 类、ModuleBase 类
- 配置文件新增 `runtime.reload_kill_connect``runtime.global_middleware_binding` 选项
- 修复部分情况下闭包事件分发时崩溃的 bug
- 新增内部方法 `_zm_env_check`
- 调整默认的 OneBot 模块对应的等级从 99999 调整为 99
- 新增导出框架运行参数的列表功能
## build 423 (2021-10-17)
- 修复 PHP 7.2 ~ 7.3 下无法使用新版 MySQL 组件的 bug
## build 422 (2021-10-6)
- 修复 `script_` 前缀无法被排除加载模块的 bug
- 修复 MySQL 组件的依赖问题
## build 421 (2021-9-11)
- 删除多余的调试信息
## build 420 (2021-9-11)
- 修复 OneBot 事件无法响应的 bug
- 新增部分 EventDispatcher 触发的事件 debug 日志
## build 419 (2021-9-11)
- 修复 DB 模块在未连接数据库的时候抛出未知异常
- 修复部分情况下打包模块出现的错误
## build 418 (2021-9-10)
- 修复 ZMAtomic 在 test 环境下的 bug
- 修复 MessageUtil 的报错
## build 417 (2021-8-29)
- 新增 AnnotationException统一框架内部的抛出异常的类型
- 新增 AnnotationParser 下的 `verifyMiddlewares()` 方法
- 私有化 CQAPI 类下的内部方法
- 将 WebSocket API 响应超时时间从 60 秒缩短为 30 秒
- 修复 DB 类不能使用旧查询器的 bug
- 统一 DB 类下抛出 Exception 的类型为 ZMException 的子类
- EventDispatcher 新增对 `middleware_error_policy` 的处理段
- 配置文件下 `runtime` 新增 `middleware_error_policy` 字段
- 将 LightCache 组件抛出的异常改为 LightCacheException
- ModuleManager 修复改配置的 `load_path` 不生效的 bug
- 修复打包时生成的 Phar Autoload 列表出错的 bug
- 将配置的 override 改为 overwrite
- 新增解包时忽略依赖的选项(`--ignore-depends`
- 删除众多调试日志,修改部分调试日志为 debug 级别的输出
- 修改 `ZM\MySQL\MySQLManager` 下的 `getConnection()``getWrapper()`
- MySQLPool 对象新增 `getCount()` 方法
- 新增 MySQLQueryBuilder 类(`doctrine/dbal` 的 wrapper 类)
- 修复 MySQLStatement 封装原 dbal 组件时与连接池不兼容的 bug
- 新增 MySQLStatementWrapper 类
- 完善 MySQLWrapper 类,用作主要的查询对象控制类
- 编写外部插件加载方式Phar 热加载功能)
- 修复 `ZMUtil::getClassesPsr4()` 方法在遇到空扩展名文件时的报错

View File

@@ -2,6 +2,15 @@
这里将会记录各个主版本的框架升级后,涉及 `global.php` 的更新日志,你可以根据这里描述的内容与你的旧配置文件进行合并。 这里将会记录各个主版本的框架升级后,涉及 `global.php` 的更新日志,你可以根据这里描述的内容与你的旧配置文件进行合并。
## v2.6.0 (build 427)
- 新增 `$config['runtime']` 下的 `reload_delay_time``global_middleware_binding` 项。
- 新增 `$config['onebot']` 下的 `message_convert_string` 项。
## v2.5.1 (build 417)
- 新增 `$config['runtime']` 下的 `middleware_error_policy` 选项。
## v2.5.0 (build 413) ## v2.5.0 (build 413)
- 新增 `$config['runtime']` 运行时设置。 - 新增 `$config['runtime']` 运行时设置。

View File

@@ -1,5 +1,105 @@
# 更新日志v2 版本) # 更新日志v2 版本)
## v2.6.1 (build 428)
> 更新时间2021.11.16
- 修复 ctx()->waitMessage() 在 array 消息模式下无法正确返回消息字符串的问题
- 新增 ctx()->getArrayMessage() 和 ctx()->getStringMessage() 两个方法
- 修复注解事件 CQCommand 和 CQMessage 在 array 消息模式下无法正确解析的 Bug
## v2.6.0 (build 427)
> 更新时间2021.11.16
- 新增全局中间件,可在全局配置文件中设置
- 修复部分 Typo
- 新增指令 `server:status``server:reload``server:stop` 可用在新开终端中查看框架运行状态、重启和退出
- 新增支持 `array` 格式的消息
- 上下文 Context 对象新增 `getOriginMessage()` 用于获取原消息,`getMessage()` 如果在设置了转换后,将默认转换消息为字符串格式保持与旧模块兼容
- OneBot API 新增全局过滤器,可用作 Action 过滤重写等操作
- 配置文件新增 `runtime.reload_delay_time`,用于可配置重载 Worker 等待的时间(毫秒)
- 配置文件新增 `runtime.global_middleware_binding`,用于配置全局中间件
- 配置文件新增 `onebot.message_convert_string`,用于配置是否转换数组格式为字符串,保证与前版本的兼容性(默认为 true
- MessageUtil 消息工具类新增方法:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
- MessageUtil 消息工具类新增方法:`arrayToStr(array $array)`
- 新增框架启动多次监测功能,无法使用同一个框架项目同时启动两个框架
## v2.5.8 (build 426)
> 更新时间2021.11.10
- 修复 CQ 码的解析函数 Bug#52
## v2.5.7 (build 425)
> 更新时间2021.11.3
- 调低 OneBot 相关事件在 Swoole 的优先级
- 修复部分情况下闭包事件函数分发时引发的崩溃 bug
- 修复 reload 时会断开 WebSocket 连接且导致进程崩溃的 bug
## v2.5.6 (build 423)
> 更新时间2021.10.17
- 修复 PHP 7.2 ~ 7.3 下无法使用新版 MySQL 组件的 bug
## v2.5.5 (build 422)
> 更新时间2021.10.6
- 修复 `script_` 前缀无法被排除加载模块的 bug
- 修复 MySQL 组件的依赖问题
## v2.5.4 (buidl 421)
> 更新时间2021.9.11
- 删除多余的调试信息
## v2.5.3 (build 420)
> 更新时间2021.9.11
- 修复 DB 模块在未连接数据库的时候抛出未知异常
- 修复部分情况下打包模块出现的错误
- 修复 OneBot 事件无法响应的 bug
- 新增部分 EventDispatcher 触发的事件 debug 日志
## v2.5.2 (build 418)
> 更新时间2021.9.10
- 新增 AnnotationException统一框架内部的抛出异常的类型
- 新增 AnnotationParser 下的 `verifyMiddlewares()` 方法
- 私有化 CQAPI 类下的内部方法
- 将 WebSocket API 响应超时时间从 60 秒缩短为 30 秒
- 修复 DB 类不能使用旧查询器的 bug
- 统一 DB 类下抛出 Exception 的类型为 ZMException 的子类
- EventDispatcher 新增对 `middleware_error_policy` 的处理段
- 配置文件下 `runtime` 新增 `middleware_error_policy` 字段
- 将 LightCache 组件抛出的异常改为 LightCacheException
- ModuleManager 修复改配置的 `load_path` 不生效的 bug
- 修复打包时生成的 Phar Autoload 列表出错的 bug
- 将配置的 override 改为 overwrite
- 新增解包时忽略依赖的选项(`--ignore-depends`
- 删除众多调试日志,修改部分调试日志为 debug 级别的输出
- 修改 `ZM\MySQL\MySQLManager` 下的 `getConnection()``getWrapper()`
- MySQLPool 对象新增 `getCount()` 方法
- 新增 MySQLQueryBuilder 类(`doctrine/dbal` 的 wrapper 类)
- 修复 MySQLStatement 封装原 dbal 组件时与连接池不兼容的 bug
- 新增 MySQLStatementWrapper 类
- 完善 MySQLWrapper 类,用作主要的查询对象控制类
- 编写外部插件加载方式Phar 热加载功能)
- 修复 `ZMUtil::getClassesPsr4()` 方法在遇到空扩展名文件时的报错
## v2.5.1 (build 416)
> 更新时间2021.7.9
- 修复:脚手架无法正常使用 `init` 命令的 bug。
## v2.5.0build 415 ## v2.5.0build 415
> 更新时间2021.7.9 > 更新时间2021.7.9
@@ -373,4 +473,4 @@
> 更新时间2020.12.23 > 更新时间2020.12.23
已发布正式版。 已发布正式版。

0
ext/go-cqhttp/go-cqhttp-down.sh Executable file → Normal file
View File

0
install-runtime.sh Executable file → Normal file
View File

25
instant-test.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
require_once "vendor/autoload.php";
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Module\InstantModule;
use ZM\ZMServer;
$weather = new InstantModule("weather");
$weather->onEvent(OnOpenEvent::class, ['connect_type' => 'qq'], function(ConnectionObject $conn) {
Console::info("机器人 " . $conn->getOption("connect_id") . " 已连接!");
});
$weather->onEvent(CQCommand::class, ['match' => '你好'], function() {
ctx()->reply("hello呀");
});
$app = new ZMServer("app-name");
$app->addModule($weather);
$app->run();

View File

@@ -14,6 +14,7 @@ theme:
accent: indigo accent: indigo
features: features:
- navigation.tabs - navigation.tabs
- navigation.sections
extra_javascript: extra_javascript:
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js
- javascripts/config.js - javascripts/config.js
@@ -66,6 +67,7 @@ nav:
- 基本配置: guide/basic-config.md - 基本配置: guide/basic-config.md
- 编写模块: guide/write-module.md - 编写模块: guide/write-module.md
- 注册事件响应: guide/register-event.md - 注册事件响应: guide/register-event.md
- 升级指南: guide/upgrade.md
- 错误码对照表: guide/errcode.md - 错误码对照表: guide/errcode.md
- 事件和注解: - 事件和注解:
- 事件和注解: event/index.md - 事件和注解: event/index.md
@@ -77,46 +79,54 @@ nav:
- 事件分发器: event/event-dispatcher.md - 事件分发器: event/event-dispatcher.md
- 框架组件: - 框架组件:
- 框架组件: component/index.md - 框架组件: component/index.md
- 上下文: component/context.md
- 聊天机器人组件: - 聊天机器人组件:
- 机器人 API: component/bot/robot-api.md - 机器人动作V12: component/bot/robot-api-12.md
- 机器人动作V11: component/bot/robot-api.md
- CQ 码(多媒体消息): component/bot/cqcode.md - CQ 码(多媒体消息): component/bot/cqcode.md
- 机器人消息处理: component/bot/message-util.md - 机器人消息处理: component/bot/message-util.md
- Token 验证: component/bot/access-token.md - Token 验证: component/bot/access-token.md
- 图灵机器人 API: component/bot/turing-api.md - 图灵机器人 API: component/bot/turing-api.md
- 存储: - 存储:
- LightCache 轻量缓存: component/store/light-cache.md - LightCache 轻量缓存: component/store/light-cache.md
- MySQL 数据库: component/store/mysql.md - MySQL 查询器:
- 简介: component/store/mysql/mysql.md
- 配置: component/store/mysql/config.md
- 执行 SQL 语句: component/store/mysql/common-query.md
- MySQL 查询器(废弃): component/store/mysql-db.md
- Redis 数据库: component/store/redis.md - Redis 数据库: component/store/redis.md
- ZMAtomic 原子计数器: component/store/atomics.md - ZMAtomic 原子计数器: component/store/atomics.md
- SpinLock 自旋锁: component/store/spin-lock.md - SpinLock 自旋锁: component/store/spin-lock.md
- 文件管理: component/store/data-provider.md - 文件管理: component/store/data-provider.md
- 通用组件:
- 上下文: component/common/context.md
- 协程池: component/common/coroutine-pool.md
- 单例类: component/common/singleton-trait.md
- ZMUtil 杂项: component/common/zmutil.md
- 全局方法: component/common/global-functions.md
- Console 终端: component/common/console.md
- TaskWorker 管理: component/common/task-worker.md
- Terminal 终端: component/common/remote-terminal.md
- HTTP 服务器工具类: - HTTP 服务器工具类:
- HTTP 和 WebSocket 客户端: component/zmrequest.md - HTTP 和 WebSocket 客户端: component/http/zmrequest.md
- HTTP 路由管理: component/route-manager.md - HTTP 路由管理: component/http/route-manager.md
- 模块/插件管理: - 模块/插件管理:
- 模块打包: component/module/module-pack.md - 模块打包: component/module/module-pack.md
- 协程池: component/coroutine-pool.md - 模块解包: component/module/module-unpack.md
- 单例类: component/singleton-trait.md
- ZMUtil 杂项: component/zmutil.md
- 全局方法: component/global-functions.md
- Console 终端: component/console.md
- TaskWorker 管理: component/task-worker.md
- Terminal 终端: component/remote-terminal.md
- 进阶开发: - 进阶开发:
- 进阶开发: advanced/index.md - 进阶开发: advanced/index.md
- 框架剖析: advanced/framework-structure.md - 框架高级开发:
- 框架启动模式: advanced/custom-start.md - 框架剖析: advanced/framework-structure.md
- 手动安装环境: advanced/manually-install.md - 框架启动模式: advanced/custom-start.md
- 从 v1 升级: advanced/to-v2.md - 手动安装环境: advanced/manually-install.md
- 内部类文件手册: advanced/inside-class.md - 内部类文件手册: advanced/inside-class.md
- 接入 WebSocket 客户端: advanced/connect-ws-client.md - 框架多进程: advanced/multi-process.md
- 框架多进程: advanced/multi-process.md - TaskWorker 提高并发: advanced/task-worker.md
- TaskWorker 提高并发: advanced/task-worker.md
- 开发实战教程: - 开发实战教程:
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
- 编写管理员才能触发的功能: advanced/example/admin.md - 编写管理员才能触发的功能: advanced/example/admin.md
- FAQ: - FAQ:
- FAQ: faq/FAQ.md - FAQ: faq/FAQ.md
- 从 v1 升级: faq/to-v2.md
- 框架常见问题(持续更新): faq/usual-question.md - 框架常见问题(持续更新): faq/usual-question.md
- 启动时报错 Address already in use: faq/address-already-in-use.md - 启动时报错 Address already in use: faq/address-already-in-use.md
- 出现 deadlock 字样: faq/display-deadlock.md - 出现 deadlock 字样: faq/display-deadlock.md
@@ -125,5 +135,6 @@ nav:
- 更新日志: - 更新日志:
- 更新日志v2: update/v2.md - 更新日志v2: update/v2.md
- 更新日志v1: update/v1.md - 更新日志v1: update/v1.md
- 更新日志build update: update/build-update.md
- 配置文件更新日志: update/config.md - 配置文件更新日志: update/config.md
- <u>炸毛框架 v1</u>: https://docs-v1.zhamao.xin/ - <u>炸毛框架 v1</u>: https://docs-v1.zhamao.xin/

View File

@@ -10,6 +10,7 @@ use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnRequestEvent; use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\API\CQ; use ZM\API\CQ;
use ZM\API\TuringAPI; use ZM\API\TuringAPI;
use ZM\API\OneBotV11;
use ZM\ConnectionManager\ConnectionObject; use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Annotation\CQ\CQCommand; use ZM\Annotation\CQ\CQCommand;
@@ -49,8 +50,12 @@ class Hello
* @CQCommand("我是谁") * @CQCommand("我是谁")
*/ */
public function whoami() { public function whoami() {
$user = ctx()->getRobot()->getLoginInfo(); $bot = ctx()->getRobot()->getLoginInfo();
return "你是" . $user["data"]["nickname"] . "QQ号是" . $user["data"]["user_id"]; $bot_id = $bot["data"]["user_id"];
$r = OneBotV11::get($bot_id);
$QQid = ctx()->getUserId();
$nick = $r->getStrangerInfo($QQid)["data"]["nickname"];
return "你是" . $nick . "QQ号是" . $QQid;
} }
/** /**

View File

@@ -33,7 +33,7 @@ class TimerMiddleware implements MiddlewareInterface
* @HandleAfter() * @HandleAfter()
*/ */
public function onAfter() { public function onAfter() {
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms."); Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 3) . " ms.");
} }
/** /**
@@ -42,7 +42,7 @@ class TimerMiddleware implements MiddlewareInterface
* @throws Exception * @throws Exception
*/ */
public function onException(Exception $e) { public function onException(Exception $e) {
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred."); Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 3) . " ms but an Exception occurred.");
throw $e; throw $e;
} }
} }

View File

@@ -356,7 +356,7 @@ class CQ
} }
$cq["start"] = $offset + $head; $cq["start"] = $offset + $head;
$cq["end"] = $offset + $tmpmsg + $head; $cq["end"] = $offset + $tmpmsg + $head;
$offset += $tmpmsg + 1; $offset += $head + $tmpmsg + 1;
$cqs[] = (!$is_object ? $cq : CQObject::fromArray($cq)); $cqs[] = (!$is_object ? $cq : CQObject::fromArray($cq));
} }
return $cqs; return $cqs;

View File

@@ -3,6 +3,7 @@
namespace ZM\API; namespace ZM\API;
use Closure;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Store\LightCacheInside; use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock; use ZM\Store\Lock\SpinLock;
@@ -11,6 +12,13 @@ use ZM\Utils\CoMessage;
trait CQAPI trait CQAPI
{ {
/** @var null|Closure */
private static $filter = null;
public static function registerFilter(callable $callable) {
self::$filter = $callable;
}
/** /**
* @param $connection * @param $connection
* @param $reply * @param $reply
@@ -18,13 +26,18 @@ trait CQAPI
* @return bool|array * @return bool|array
*/ */
private function processAPI($connection, $reply, $function = null) { private function processAPI($connection, $reply, $function = null) {
if (is_callable(self::$filter)) {
$reply2 = call_user_func(self::$filter, $reply);
if (is_bool($reply2)) return $reply2;
else $reply = $reply2;
}
if ($connection->getOption("type") === CONN_WEBSOCKET) if ($connection->getOption("type") === CONN_WEBSOCKET)
return $this->processWebsocketAPI($connection, $reply, $function); return $this->processWebsocketAPI($connection, $reply, $function);
else else
return $this->processHttpAPI($connection, $reply, $function); return $this->processHttpAPI($connection, $reply, $function);
} }
public function processWebsocketAPI($connection, $reply, $function = false) { private function processWebsocketAPI($connection, $reply, $function = false) {
$api_id = ZMAtomic::get("wait_msg_id")->add(1); $api_id = ZMAtomic::get("wait_msg_id")->add(1);
$reply["echo"] = $api_id; $reply["echo"] = $api_id;
if (server()->push($connection->getFd(), json_encode($reply))) { if (server()->push($connection->getFd(), json_encode($reply))) {
@@ -35,7 +48,7 @@ trait CQAPI
"self_id" => $connection->getOption("connect_id"), "self_id" => $connection->getOption("connect_id"),
"echo" => $api_id "echo" => $api_id
]; ];
return CoMessage::yieldByWS($obj, ["echo"], 60); return CoMessage::yieldByWS($obj, ["echo"], 30);
} }
return true; return true;
} else { } else {
@@ -63,7 +76,7 @@ trait CQAPI
* @return bool * @return bool
* @noinspection PhpUnusedParameterInspection * @noinspection PhpUnusedParameterInspection
*/ */
public function processHttpAPI($connection, $reply, $function = null): bool { private function processHttpAPI($connection, $reply, $function = null): bool {
return false; return false;
} }

View File

@@ -4,7 +4,9 @@
namespace ZM\Annotation; namespace ZM\Annotation;
use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationReader;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Interfaces\ErgodicAnnotation; use ZM\Annotation\Interfaces\ErgodicAnnotation;
use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
use ReflectionClass; use ReflectionClass;
use ReflectionException; use ReflectionException;
@@ -17,6 +19,7 @@ use ZM\Annotation\Http\MiddlewareClass;
use ZM\Annotation\Http\RequestMapping; use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Interfaces\Level; use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Module\Closed; use ZM\Annotation\Module\Closed;
use ZM\Exception\AnnotationException;
use ZM\Utils\Manager\RouteManager; use ZM\Utils\Manager\RouteManager;
use ZM\Utils\ZMUtil; use ZM\Utils\ZMUtil;
@@ -53,7 +56,7 @@ class AnnotationParser
*/ */
public function registerMods() { public function registerMods() {
foreach ($this->path_list as $path) { foreach ($this->path_list as $path) {
Console::debug("parsing annotation in " . $path[0]); Console::debug("parsing annotation in " . $path[0].":".$path[1]);
$all_class = ZMUtil::getClassesPsr4($path[0], $path[1]); $all_class = ZMUtil::getClassesPsr4($path[0], $path[1]);
$this->reader = new AnnotationReader(); $this->reader = new AnnotationReader();
foreach ($all_class as $v) { foreach ($all_class as $v) {
@@ -96,7 +99,6 @@ class AnnotationParser
if ($vs instanceof ErgodicAnnotation) { if ($vs instanceof ErgodicAnnotation) {
foreach (($this->annotation_map[$v]["methods"] ?? []) as $method) { foreach (($this->annotation_map[$v]["methods"] ?? []) as $method) {
$copy = clone $vs; $copy = clone $vs;
/** @noinspection PhpUndefinedFieldInspection */
$copy->method = $method->getName(); $copy->method = $method->getName();
$this->annotation_map[$v]["methods_annotations"][$method->getName()][] = $copy; $this->annotation_map[$v]["methods_annotations"][$method->getName()][] = $copy;
} }
@@ -107,18 +109,34 @@ class AnnotationParser
unset($this->annotation_map[$v]); unset($this->annotation_map[$v]);
continue 2; continue 2;
} elseif ($vs instanceof MiddlewareClass) { } elseif ($vs instanceof MiddlewareClass) {
//注册中间件本身的类,标记到 middlewares 属性中
Console::debug("正在注册中间件 " . $reflection_class->getName()); Console::debug("正在注册中间件 " . $reflection_class->getName());
$rs = $this->registerMiddleware($vs, $reflection_class); $rs = $this->registerMiddleware($vs, $reflection_class);
$this->middlewares[$rs["name"]] = $rs; $this->middlewares[$rs["name"]] = $rs;
} }
} }
$inserted = [];
//预处理3处理每个函数上面的特殊注解就是需要操作一些东西的 //预处理3处理每个函数上面的特殊注解就是需要操作一些东西的
foreach (($this->annotation_map[$v]["methods_annotations"] ?? []) as $method_name => $methods_annotations) { foreach (($this->annotation_map[$v]["methods_annotations"] ?? []) as $method_name => $methods_annotations) {
foreach ($methods_annotations as $method_anno) { foreach ($methods_annotations as $method_anno) {
/** @var AnnotationBase $method_anno */ /** @var AnnotationBase $method_anno */
$method_anno->class = $v; $method_anno->class = $v;
$method_anno->method = $method_name; $method_anno->method = $method_name;
if (!($method_anno instanceof Middleware) && ($middlewares = ZMConfig::get("global", "runtime")["global_middleware_binding"][get_class($method_anno)] ?? []) !== []) {
if (!isset($inserted[$v][$method_name])) {
// 在这里在其他中间件前插入插入全局的中间件
foreach ($middlewares as $middleware) {
$mid_class = new Middleware();
$mid_class->middleware = $middleware;
$mid_class->class = $v;
$mid_class->method = $method_name;
$this->middleware_map[$v][$method_name][] = $mid_class;
}
$inserted[$v][$method_name] = true;
}
}
if ($method_anno instanceof RequestMapping) { if ($method_anno instanceof RequestMapping) {
RouteManager::importRouteByAnnotation($method_anno, $method_name, $v, $methods_annotations); RouteManager::importRouteByAnnotation($method_anno, $method_name, $v, $methods_annotations);
} elseif ($method_anno instanceof Middleware) { } elseif ($method_anno instanceof Middleware) {
@@ -131,9 +149,10 @@ class AnnotationParser
Console::debug("解析注解完毕!"); Console::debug("解析注解完毕!");
} }
public function generateAnnotationEvents() { public function generateAnnotationEvents(): array {
$o = []; $o = [];
foreach ($this->annotation_map as $obj) { foreach ($this->annotation_map as $obj) {
// 这里的ErgodicAnnotation是为了解决类上的注解可穿透到方法上的问题
foreach (($obj["class_annotations"] ?? []) as $class_annotation) { foreach (($obj["class_annotations"] ?? []) as $class_annotation) {
if ($class_annotation instanceof ErgodicAnnotation) continue; if ($class_annotation instanceof ErgodicAnnotation) continue;
else $o[get_class($class_annotation)][] = $class_annotation; else $o[get_class($class_annotation)][] = $class_annotation;
@@ -193,6 +212,12 @@ class AnnotationParser
return $result; return $result;
} }
/**
* @internal 用于 level 排序
* @param $events
* @param string $class_name
* @param string $prefix
*/
public function sortByLevel(&$events, string $class_name, $prefix = "") { public function sortByLevel(&$events, string $class_name, $prefix = "") {
if (is_a($class_name, Level::class, true)) { if (is_a($class_name, Level::class, true)) {
$class_name .= $prefix; $class_name .= $prefix;
@@ -203,4 +228,22 @@ class AnnotationParser
}); });
} }
} }
/**
* @throws AnnotationException
*/
public function verifyMiddlewares() {
if ((ZMConfig::get("global", "runtime")["middleware_error_policy"] ?? 1) === 2) {
//我承认套三层foreach很不优雅但是这个会很快的。
foreach($this->middleware_map as $class => $v) {
foreach ($v as $method => $vs) {
foreach($vs as $mid) {
if (!isset($this->middlewares[$mid->middleware])) {
throw new AnnotationException("Annotation parse error: Unknown MiddlewareClass named \"{$mid->middleware}\"!");
}
}
}
}
}
}
} }

View File

@@ -12,7 +12,7 @@ class DaemonReloadCommand extends DaemonCommand
protected static $defaultName = 'daemon:reload'; protected static $defaultName = 'daemon:reload';
protected function configure() { protected function configure() {
$this->setDescription("重载守护进程下的用户代码(仅限--daemon模式可用"); $this->setDescription("重载框架");
} }
protected function execute(InputInterface $input, OutputInterface $output): int { protected function execute(InputInterface $input, OutputInterface $output): int {

View File

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

View File

@@ -13,7 +13,7 @@ class DaemonStopCommand extends DaemonCommand
protected static $defaultName = 'daemon:stop'; protected static $defaultName = 'daemon:stop';
protected function configure() { protected function configure() {
$this->setDescription("停止守护进程下运行的框架(仅限--daemon模式可用"); $this->setDescription("停止运行的框架");
} }
protected function execute(InputInterface $input, OutputInterface $output): int { protected function execute(InputInterface $input, OutputInterface $output): int {

View File

@@ -18,8 +18,8 @@ class ModuleListCommand extends Command
protected static $defaultName = 'module:list'; protected static $defaultName = 'module:list';
protected function configure() { protected function configure() {
$this->setDescription("Build an \".phar\" file | 将项目构建一个phar包"); $this->setDescription("查看所有模块信息");
$this->setHelp("此功能将会把炸毛框架的模块打包为\".phar\",供发布和执行"); $this->setHelp("此功能将会把炸毛框架的模块列举出来");
// ... // ...
} }
@@ -31,6 +31,7 @@ class ModuleListCommand extends Command
} }
//定义常量 //定义常量
/** @noinspection PhpIncludeInspection */
include_once DataProvider::getFrameworkRootDir() . "/src/ZM/global_defines.php"; include_once DataProvider::getFrameworkRootDir() . "/src/ZM/global_defines.php";
Console::init( Console::init(
@@ -47,7 +48,7 @@ class ModuleListCommand extends Command
foreach ($list as $v) { foreach ($list as $v) {
echo "[" . Console::setColor($v["name"], "green") . "]" . PHP_EOL; echo "[" . Console::setColor($v["name"], "green") . "]" . PHP_EOL;
$out_list = ["类型" => "source"]; $out_list = ["类型" => "源码(source)"];
if (isset($v["version"])) $out_list["版本"] = $v["version"]; if (isset($v["version"])) $out_list["版本"] = $v["version"];
if (isset($v["description"])) $out_list["描述"] = $v["description"]; if (isset($v["description"])) $out_list["描述"] = $v["description"];
$out_list["目录"] = str_replace(DataProvider::getSourceRootDir() . "/", "", $v["module-path"]); $out_list["目录"] = str_replace(DataProvider::getSourceRootDir() . "/", "", $v["module-path"]);
@@ -59,7 +60,7 @@ class ModuleListCommand extends Command
$list = ModuleManager::getPackedModules(); $list = ModuleManager::getPackedModules();
foreach ($list as $v) { foreach ($list as $v) {
echo "[" . Console::setColor($v["name"], "gold") . "]" . PHP_EOL; echo "[" . Console::setColor($v["name"], "gold") . "]" . PHP_EOL;
$out_list = ["类型" => "archive(phar)"]; $out_list = ["类型" => "模块包(phar)"];
if (isset($v["module-config"]["version"])) $out_list["版本"] = $v["module-config"]["version"]; if (isset($v["module-config"]["version"])) $out_list["版本"] = $v["module-config"]["version"];
if (isset($v["module-config"]["description"])) $out_list["描述"] = $v["module-config"]["description"]; if (isset($v["module-config"]["description"])) $out_list["描述"] = $v["module-config"]["description"];
$out_list["位置"] = str_replace(DataProvider::getSourceRootDir() . "/", "", $v["phar-path"]); $out_list["位置"] = str_replace(DataProvider::getSourceRootDir() . "/", "", $v["phar-path"]);

View File

@@ -21,7 +21,7 @@ class ModulePackCommand extends Command
protected function configure() { protected function configure() {
$this->addArgument("module-name", InputArgument::REQUIRED); $this->addArgument("module-name", InputArgument::REQUIRED);
$this->setDescription("Build an \".phar\" file | 将项目构建一个phar包"); $this->setDescription("将配置好的模块构建一个phar包");
$this->setHelp("此功能将会把炸毛框架的模块打包为\".phar\",供发布和执行。"); $this->setHelp("此功能将会把炸毛框架的模块打包为\".phar\",供发布和执行。");
$this->addOption("target", "D", InputOption::VALUE_REQUIRED, "Output Directory | 指定输出目录"); $this->addOption("target", "D", InputOption::VALUE_REQUIRED, "Output Directory | 指定输出目录");
// ... // ...
@@ -52,7 +52,7 @@ class ModulePackCommand extends Command
$output->writeln("<error>不存在模块 ".$input->getArgument("module-name")." !</error>"); $output->writeln("<error>不存在模块 ".$input->getArgument("module-name")." !</error>");
return 1; return 1;
} }
$result = ModuleManager::packModule($list[$input->getArgument("module-name")]); $result = ModuleManager::packModule($list[$input->getArgument("module-name")], $input->getOption("target"));
if ($result) Console::success("打包完成!"); if ($result) Console::success("打包完成!");
else Console::error("打包失败!"); else Console::error("打包失败!");
return 0; return 0;

View File

@@ -22,11 +22,12 @@ class ModuleUnpackCommand extends Command
protected function configure() { protected function configure() {
$this->setDefinition([ $this->setDefinition([
new InputArgument("module-name", InputArgument::REQUIRED), new InputArgument("module-name", InputArgument::REQUIRED),
new InputOption("override-light-cache", null, null, "覆盖现有的LightCache项目"), new InputOption("overwrite-light-cache", null, null, "覆盖现有的LightCache项目"),
new InputOption("override-zm-data", null, null, "覆盖现有的zm_data文件"), new InputOption("overwrite-zm-data", null, null, "覆盖现有的zm_data文件"),
new InputOption("override-source", null, null, "覆盖现有的源码文件") new InputOption("overwrite-source", null, null, "覆盖现有的源码文件"),
new InputOption("ignore-depends", null, null, "解包时忽略检查依赖")
]); ]);
$this->setDescription("Unpack a phar module into src directory"); $this->setDescription("解包一个phar模块到src目录");
$this->setHelp("此功能将phar格式的模块包解包到src目录下。"); $this->setHelp("此功能将phar格式的模块包解包到src目录下。");
// ... // ...
} }
@@ -39,6 +40,7 @@ class ModuleUnpackCommand extends Command
} }
//定义常量 //定义常量
/** @noinspection PhpIncludeInspection */
include_once DataProvider::getFrameworkRootDir()."/src/ZM/global_defines.php"; include_once DataProvider::getFrameworkRootDir()."/src/ZM/global_defines.php";
Console::init( Console::init(

View File

@@ -8,12 +8,14 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use ZM\Framework; use ZM\Framework;
use ZM\Utils\DataProvider;
class RunServerCommand extends Command class RunServerCommand extends Command
{ {
protected static $defaultName = 'server'; protected static $defaultName = 'server';
protected function configure() { protected function configure() {
$this->setAliases(['server:start']);
$this->setDefinition([ $this->setDefinition([
new InputOption("debug-mode", "D", null, "开启调试模式 (这将关闭协程化)"), new InputOption("debug-mode", "D", null, "开启调试模式 (这将关闭协程化)"),
new InputOption("log-debug", null, null, "调整消息等级到debug (log-level=4)"), new InputOption("log-debug", null, null, "调整消息等级到debug (log-level=4)"),
@@ -47,7 +49,24 @@ class RunServerCommand extends Command
return 1; return 1;
} }
} }
$pid_path = DataProvider::getWorkingDir() . "/.daemon_pid";
if (file_exists($pid_path)) {
$pid = json_decode(file_get_contents($pid_path), true)["pid"] ?? null;
if ($pid !== null && posix_getsid($pid) !== false) {
$output->writeln("<error>检测到已经在 pid: $pid 进程启动了框架!</error>");
$output->writeln("<error>不可以同时启动两个框架!</error>");
return 1;
} else {
unlink($pid_path);
}
}
(new Framework($input->getOptions()))->start(); (new Framework($input->getOptions()))->start();
return 0; return 0;
} }
public static function exportDefinition() {
$cmd = new self();
$cmd->configure();
return $cmd->getDefinition();
}
} }

View File

@@ -0,0 +1,25 @@
<?php
namespace ZM\Command\Server;
use Swoole\Process;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Command\Daemon\DaemonCommand;
class ServerReloadCommand extends DaemonCommand
{
protected static $defaultName = 'server:reload';
protected function configure() {
$this->setDescription("重载框架");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
parent::execute($input, $output);
Process::kill(intval($this->daemon_file["pid"]), SIGUSR1);
$output->writeln("<info>成功重载!</info>");
return 0;
}
}

View File

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

View File

@@ -0,0 +1,35 @@
<?php
namespace ZM\Command\Server;
use Swoole\Process;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Command\Daemon\DaemonCommand;
use ZM\Utils\DataProvider;
class ServerStopCommand extends DaemonCommand
{
protected static $defaultName = 'server:stop';
protected function configure() {
$this->setDescription("停止运行的框架");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
parent::execute($input, $output);
Process::kill(intval($this->daemon_file["pid"]), SIGTERM);
$i = 10;
while (file_exists(DataProvider::getWorkingDir() . "/.daemon_pid") && $i > 0) {
sleep(1);
--$i;
}
if ($i === 0) {
$output->writeln("<error>停止失败请检查进程pid #" . $this->daemon_file["pid"] . " 是否响应!</error>");
} else {
$output->writeln("<info>成功停止!</info>");
}
return 0;
}
}

View File

@@ -21,15 +21,17 @@ use ZM\Command\RunServerCommand;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use ZM\Console\Console; use ZM\Command\Server\ServerReloadCommand;
use ZM\Command\Server\ServerStatusCommand;
use ZM\Command\Server\ServerStopCommand;
use ZM\Exception\InitException; use ZM\Exception\InitException;
class ConsoleApplication extends Application class ConsoleApplication extends Application
{ {
private static $obj = null; private static $obj = null;
const VERSION_ID = 416; const VERSION_ID = 428;
const VERSION = "2.5.1"; const VERSION = "2.6.1";
/** /**
* @throws InitException * @throws InitException
@@ -43,11 +45,14 @@ class ConsoleApplication extends Application
} }
/** /**
* @param string $with_default_cmd
* @return ConsoleApplication
* @throws InitException * @throws InitException
*/ */
public function initEnv($with_default_cmd = ""): ConsoleApplication { public function initEnv(string $with_default_cmd = ""): ConsoleApplication {
if (defined("WORKDING_DIR")) throw new InitException(); if (defined("WORKING_DIR")) throw new InitException();
$this->selfCheck();
_zm_env_check();
define("WORKING_DIR", getcwd()); define("WORKING_DIR", getcwd());
if (Phar::running() !== "") { if (Phar::running() !== "") {
@@ -70,7 +75,7 @@ class ConsoleApplication extends Application
$composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom"; $composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom";
$r = file_put_contents(WORKING_DIR . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); $r = file_put_contents(WORKING_DIR . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
if ($r !== false) { if ($r !== false) {
echo "成功添加!请运行 composer dump-autoload \n"; echo "成功添加!请运行 'composer dump-autoload'\n";
exit(0); exit(0);
} else { } else {
echo zm_internal_errcode("E00006") . "添加失败!请按任意键继续!"; echo zm_internal_errcode("E00006") . "添加失败!请按任意键继续!";
@@ -88,6 +93,9 @@ class ConsoleApplication extends Application
new DaemonReloadCommand(), new DaemonReloadCommand(),
new DaemonStopCommand(), new DaemonStopCommand(),
new RunServerCommand(), //运行主服务的指令控制器 new RunServerCommand(), //运行主服务的指令控制器
new ServerStatusCommand(),
new ServerStopCommand(),
new ServerReloadCommand(),
new PureHttpCommand(), //纯HTTP服务器指令 new PureHttpCommand(), //纯HTTP服务器指令
new SystemdGenerateCommand() new SystemdGenerateCommand()
]); ]);
@@ -119,17 +127,4 @@ class ConsoleApplication extends Application
die(zm_internal_errcode("E00005") . "{$e->getMessage()} at {$e->getFile()}({$e->getLine()})"); die(zm_internal_errcode("E00005") . "{$e->getMessage()} at {$e->getFile()}({$e->getLine()})");
} }
} }
/**
* 启动炸毛前要做的环境检查项目
*/
private function selfCheck(): void {
if (!extension_loaded("swoole")) die(zm_internal_errcode("E00001") . "Can not find swoole extension.\n");
if (version_compare(SWOOLE_VERSION, "4.5.0") == -1) die(zm_internal_errcode("E00002") . "You must install swoole version >= 4.5.0 !");
if (version_compare(PHP_VERSION, "7.2") == -1) die(zm_internal_errcode("E00003") . "PHP >= 7.2 required.");
if (version_compare(SWOOLE_VERSION, "4.6.7") < 0 && !extension_loaded("pcntl")) {
Console::error(zm_internal_errcode("E00004") . "Swoole 版本必须不低于 4.6.7 或 PHP 安装加载了 pcntl 扩展!");
die();
}
}
} }

View File

@@ -9,6 +9,7 @@ use Swoole\Coroutine;
use Swoole\Http\Request; use Swoole\Http\Request;
use Swoole\WebSocket\Frame; use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server; use Swoole\WebSocket\Server;
use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ConnectionObject; use ZM\ConnectionManager\ConnectionObject;
use ZM\ConnectionManager\ManagerGM; use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console; use ZM\Console\Console;
@@ -19,6 +20,7 @@ use ZM\Exception\WaitTimeoutException;
use ZM\Http\Response; use ZM\Http\Response;
use ZM\API\ZMRobot; use ZM\API\ZMRobot;
use ZM\Utils\CoMessage; use ZM\Utils\CoMessage;
use ZM\Utils\MessageUtil;
class Context implements ContextInterface class Context implements ContextInterface
{ {
@@ -56,8 +58,8 @@ class Context implements ContextInterface
*/ */
public function getResponse(): ?Response { return self::$context[$this->cid]["response"] ?? null; } public function getResponse(): ?Response { return self::$context[$this->cid]["response"] ?? null; }
/** @return ConnectionObject|null|Response */ /** @return ConnectionObject|null */
public function getConnection() { return ManagerGM::get($this->getFd()); } public function getConnection(): ?ConnectionObject { return ManagerGM::get($this->getFd()); }
/** /**
* @return int|null * @return int|null
@@ -72,9 +74,20 @@ class Context implements ContextInterface
return $conn instanceof ConnectionObject ? new ZMRobot($conn) : null; return $conn instanceof ConnectionObject ? new ZMRobot($conn) : null;
} }
public function getMessage() { return self::$context[$this->cid]["data"]["message"] ?? null; } public function getMessage() {
if ((ZMConfig::get("global", "onebot")["message_convert_string"] ?? true) === true && is_array($msg = $this->getOriginMessage())) {
return MessageUtil::arrayToStr($msg);
} else {
return self::$context[$this->cid]["data"]["message"] ?? null;
}
}
public function setMessage($msg) { self::$context[$this->cid]["data"]["message"] = $msg; } public function setMessage($msg) {
if (is_string($msg) && is_array($this->getOriginMessage())) {
$msg = MessageUtil::strToArray($msg);
}
self::$context[$this->cid]["data"]["message"] = $msg;
}
public function getUserId() { return $this->getData()["user_id"] ?? null; } public function getUserId() { return $this->getData()["user_id"] ?? null; }
@@ -168,7 +181,11 @@ class Context implements ContextInterface
if ($r === false) { if ($r === false) {
throw new WaitTimeoutException($this, $timeout_prompt); throw new WaitTimeoutException($this, $timeout_prompt);
} }
return $r["message"]; if (is_array($r["message"]) && (ZMConfig::get("global", "onebot")["message_convert_string"] ?? true) === true) {
return MessageUtil::arrayToStr($r["message"]);
} else {
return $r["message"];
}
} }
/** /**
@@ -239,4 +256,18 @@ class Context implements ContextInterface
public function copy() { return self::$context[$this->cid]; } public function copy() { return self::$context[$this->cid]; }
public function getOption() { return self::getCache("match"); } public function getOption() { return self::getCache("match"); }
public function getOriginMessage() { return self::$context[$this->cid]["data"]["message"] ?? null; }
public function getArrayMessage(): array {
$msg = $this->getOriginMessage();
if (is_array($msg)) return $msg;
else return MessageUtil::strToArray($msg);
}
public function getStringMessage(): string {
$msg = $this->getOriginMessage();
if (is_string($msg)) return $msg;
else return MessageUtil::arrayToStr($msg);
}
} }

View File

@@ -6,9 +6,8 @@
namespace ZM\DB; namespace ZM\DB;
use Exception;
use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\MySQL\MySQLManager;
use ZM\Store\MySQL\SqlPoolStorage; use ZM\Store\MySQL\SqlPoolStorage;
use PDOException; use PDOException;
use PDOStatement; use PDOStatement;
@@ -25,12 +24,12 @@ class DB
private static $table_list = []; private static $table_list = [];
/** /**
* @param $db_name
* @throws DbException * @throws DbException
* @throws Exception
*/ */
public static function initTableList() { public static function initTableList($db_name) {
if (!extension_loaded("mysqlnd")) throw new Exception("Can not find mysqlnd PHP extension."); if (!extension_loaded("mysqlnd")) throw new DbException("Can not find mysqlnd PHP extension.");
$result = self::rawQuery("select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='" . ZMConfig::get("global", "sql_config")["sql_database"] . "';", []); $result = MySQLManager::getWrapper()->fetchAllAssociative("select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=?;", [$db_name]);
foreach ($result as $v) { foreach ($result as $v) {
self::$table_list[] = $v['TABLE_NAME']; self::$table_list[] = $v['TABLE_NAME'];
} }
@@ -94,6 +93,7 @@ class DB
if (!is_array($params)) $params = [$params]; if (!is_array($params)) $params = [$params];
Console::debug("MySQL: " . $line . " | " . implode(", ", $params)); Console::debug("MySQL: " . $line . " | " . implode(", ", $params));
try { try {
if (SqlPoolStorage::$sql_pool === null) throw new DbException("未连接到任何数据库!");
$conn = SqlPoolStorage::$sql_pool->getConnection(); $conn = SqlPoolStorage::$sql_pool->getConnection();
if ($conn === false) { if ($conn === false) {
SqlPoolStorage::$sql_pool->putConnection(null); SqlPoolStorage::$sql_pool->putConnection(null);

View File

@@ -4,9 +4,11 @@
namespace ZM\Event; namespace ZM\Event;
use Closure;
use Doctrine\Common\Annotations\AnnotationException; use Doctrine\Common\Annotations\AnnotationException;
use Error; use Error;
use Exception; use Exception;
use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Exception\InterruptException; use ZM\Exception\InterruptException;
use ZM\Store\LightCacheInside; use ZM\Store\LightCacheInside;
@@ -70,11 +72,13 @@ class EventDispatcher
} }
public function setRuleFunction(callable $rule = null): EventDispatcher { public function setRuleFunction(callable $rule = null): EventDispatcher {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 设置事件rule: " . $this->class);
$this->rule = $rule; $this->rule = $rule;
return $this; return $this;
} }
public function setReturnFunction(callable $return_func): EventDispatcher { public function setReturnFunction(callable $return_func): EventDispatcher {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 设置事件returnFunc: " . $this->class);
$this->return_func = $return_func; $this->return_func = $return_func;
return $this; return $this;
} }
@@ -87,7 +91,7 @@ class EventDispatcher
try { try {
foreach ((EventManager::$events[$this->class] ?? []) as $v) { foreach ((EventManager::$events[$this->class] ?? []) as $v) {
$this->dispatchEvent($v, $this->rule, ...$params); $this->dispatchEvent($v, $this->rule, ...$params);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。"); if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . (is_string($v->method) ? $v->method : "{closure}") . " 分发结束。");
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 (is_callable($this->return_func) && $this->status === self::STATUS_NORMAL) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ..."); if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ...");
@@ -117,7 +121,7 @@ class EventDispatcher
public function dispatchEvent($v, $rule_func = null, ...$params) { public function dispatchEvent($v, $rule_func = null, ...$params) {
$q_c = $v->class; $q_c = $v->class;
$q_f = $v->method; $q_f = $v->method;
if ($q_c === "" && ($q_f instanceof \Closure)) { if (($q_c ?? "") === "" && ($q_f instanceof Closure)) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 闭包函数的事件触发过程!"); if ($this->log) Console::verbose("[事件分发{$this->eid}] 闭包函数的事件触发过程!");
if ($rule_func !== null && !$rule_func($v)) { if ($rule_func !== null && !$rule_func($v)) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 闭包函数下的 ruleFunc 判断为 false, 拒绝执行此方法。"); if ($this->log) Console::verbose("[事件分发{$this->eid}] 闭包函数下的 ruleFunc 判断为 false, 拒绝执行此方法。");
@@ -137,11 +141,16 @@ class EventDispatcher
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 ruleFunc 为真,继续执行方法本身 ..."); if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 ruleFunc 为真,继续执行方法本身 ...");
if (isset(EventManager::$middleware_map[$q_c][$q_f])) { if (isset(EventManager::$middleware_map[$q_c][$q_f])) {
$middlewares = EventManager::$middleware_map[$q_c][$q_f]; $middlewares = EventManager::$middleware_map[$q_c][$q_f];
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法还绑定了 Middleware" . implode(", ", array_map(function($x){ return $x->middleware; }, $middlewares))); if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法还绑定了 Middleware" . implode(", ", array_map(function ($x) { return $x->middleware; }, $middlewares)));
$before_result = true; $before_result = true;
$r = []; $r = [];
foreach ($middlewares as $k => $middleware) { foreach ($middlewares as $k => $middleware) {
if (!isset(EventManager::$middlewares[$middleware->middleware])) throw new AnnotationException("Annotation parse error: Unknown MiddlewareClass named \"{$middleware->middleware}\"!"); if (!isset(EventManager::$middlewares[$middleware->middleware])) {
if ((ZMConfig::get("global", "runtime")["middleware_error_policy"] ?? 1) == 1)
throw new AnnotationException("Annotation parse error: Unknown MiddlewareClass named \"{$middleware->middleware}\"!");
else
continue;
}
$middleware_obj = EventManager::$middlewares[$middleware->middleware]; $middleware_obj = EventManager::$middlewares[$middleware->middleware];
$before = $middleware_obj["class"]; $before = $middleware_obj["class"];
//var_dump($middleware_obj); //var_dump($middleware_obj);
@@ -149,6 +158,7 @@ class EventDispatcher
$r[$k]->class = $q_c; $r[$k]->class = $q_c;
$r[$k]->method = $q_f; $r[$k]->method = $q_f;
$r[$k]->middleware = $middleware; $r[$k]->middleware = $middleware;
$r[$k]->current_event = $v;
if (isset($middleware_obj["before"])) { if (isset($middleware_obj["before"])) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 存在前置事件,执行中 ..."); if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 存在前置事件,执行中 ...");
$rs = $middleware_obj["before"]; $rs = $middleware_obj["before"];
@@ -186,7 +196,8 @@ class EventDispatcher
} }
throw $e; throw $e;
} }
for ($i = count($middlewares) - 1; $i >= 0; --$i) { $cnts = count($middlewares) - 1;
for ($i = $cnts; $i >= 0; --$i) {
$middleware_obj = EventManager::$middlewares[$middlewares[$i]->middleware]; $middleware_obj = EventManager::$middlewares[$middlewares[$i]->middleware];
if (isset($middleware_obj["after"], $r[$i])) { if (isset($middleware_obj["after"], $r[$i])) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 存在后置事件,执行中 ..."); if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 存在后置事件,执行中 ...");

View File

@@ -12,6 +12,7 @@ use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnTick; use ZM\Annotation\Swoole\OnTick;
use ZM\Config\ZMConfig; use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Exception\AnnotationException;
use ZM\Store\LightCache; use ZM\Store\LightCache;
use ZM\Store\ZMAtomic; use ZM\Store\ZMAtomic;
@@ -23,15 +24,24 @@ class EventManager
public static $req_mapping = []; public static $req_mapping = [];
public static function addEvent($event_name, ?AnnotationBase $event_obj) { public static function addEvent($event_name, ?AnnotationBase $event_obj) {
if ($event_obj->method instanceof \Closure) {
Console::debug("Adding event $event_name at @Anonymous");
} else {
Console::debug("Adding event $event_name at " . ($event_obj->class) . ":" . ($event_obj->method));
}
self::$events[$event_name][] = $event_obj; self::$events[$event_name][] = $event_obj;
(new AnnotationParser())->sortByLevel(self::$events, $event_name); (new AnnotationParser())->sortByLevel(self::$events, $event_name);
} }
/**
* @throws AnnotationException
*/
public static function loadEventByParser(AnnotationParser $parser) { public static function loadEventByParser(AnnotationParser $parser) {
self::$events = $parser->generateAnnotationEvents(); self::$events = $parser->generateAnnotationEvents();
self::$middlewares = $parser->getMiddlewares(); self::$middlewares = $parser->getMiddlewares();
self::$middleware_map = $parser->getMiddlewareMap(); self::$middleware_map = $parser->getMiddlewareMap();
self::$req_mapping = $parser->getReqMapping(); self::$req_mapping = $parser->getReqMapping();
$parser->verifyMiddlewares();
} }
/** /**

View File

@@ -7,7 +7,7 @@ namespace ZM\Event\SwooleEvent;
use Swoole\Process; use Swoole\Process;
use Swoole\WebSocket\Server; use Swoole\WebSocket\Server;
use ZM\Annotation\Swoole\SwooleHandler; use ZM\Annotation\Swoole\SwooleHandler;
use ZM\ConnectionManager\ManagerGM; use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Event\SwooleEvent; use ZM\Event\SwooleEvent;
@@ -23,11 +23,7 @@ class OnBeforeReload implements SwooleEvent
for ($i = 0; $i < ZM_WORKER_NUM; ++$i) { for ($i = 0; $i < ZM_WORKER_NUM; ++$i) {
Process::kill(zm_atomic("_#worker_" . $i)->get(), SIGUSR1); Process::kill(zm_atomic("_#worker_" . $i)->get(), SIGUSR1);
} }
foreach ($server->connections as $fd) { $conf = ZMConfig::get("global", "runtime")["reload_delay_time"] ?? 800;
if (ManagerGM::get($fd) !== null) $server->disconnect($fd); if ($conf !== 0) usleep($conf * 1000);
else $server->close($fd);
}
usleep(800 * 1000);
} }
} }

View File

@@ -25,6 +25,9 @@ use ZM\Event\SwooleEvent;
*/ */
class OnMessage implements SwooleEvent class OnMessage implements SwooleEvent
{ {
/**
* @noinspection PhpUnreachableStatementInspection
*/
public function onCall($server, Frame $frame) { public function onCall($server, Frame $frame) {
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd . ": " . TermColor::ITALIC . $frame->data . TermColor::RESET); Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd . ": " . TermColor::ITALIC . $frame->data . TermColor::RESET);
unset(Context::$context[Coroutine::getCid()]); unset(Context::$context[Coroutine::getCid()]);
@@ -32,8 +35,8 @@ class OnMessage implements SwooleEvent
set_coroutine_params(["server" => $server, "frame" => $frame, "connection" => $conn]); set_coroutine_params(["server" => $server, "frame" => $frame, "connection" => $conn]);
$dispatcher1 = new EventDispatcher(OnMessageEvent::class); $dispatcher1 = new EventDispatcher(OnMessageEvent::class);
$dispatcher1->setRuleFunction(function ($v) { $dispatcher1->setRuleFunction(function ($v) {
/** @noinspection PhpUnreachableStatementInspection */ if ($v->getRule() == '') return true;
return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";"); else return eval("return " . $v->getRule() . ";");
}); });

View File

@@ -70,7 +70,7 @@ class OnOpen implements SwooleEvent
try { try {
$obb_onebot = ZMConfig::get("global", "onebot") ?? $obb_onebot = ZMConfig::get("global", "onebot") ??
ZMConfig::get("global", "modules")["onebot"] ?? ZMConfig::get("global", "modules")["onebot"] ??
["status" => true, "single_bot_mode" => false, "message_level" => 99999]; ["status" => true, "single_bot_mode" => false, "message_level" => 99];
$onebot_status = $obb_onebot["status"]; $onebot_status = $obb_onebot["status"];
if ($conn->getName() === 'qq' && $onebot_status === true) { if ($conn->getName() === 'qq' && $onebot_status === true) {
if ($obb_onebot["single_bot_mode"]) { if ($obb_onebot["single_bot_mode"]) {

View File

@@ -8,6 +8,7 @@ use Swoole\Server;
use ZM\Annotation\Swoole\SwooleHandler; use ZM\Annotation\Swoole\SwooleHandler;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Event\SwooleEvent; use ZM\Event\SwooleEvent;
use ZM\Utils\DataProvider;
/** /**
* Class OnShutdown * Class OnShutdown
@@ -18,5 +19,9 @@ class OnShutdown implements SwooleEvent
{ {
public function onCall(Server $server) { public function onCall(Server $server) {
Console::verbose("正在关闭 Master 进程pid=" . posix_getpid()); Console::verbose("正在关闭 Master 进程pid=" . posix_getpid());
$pid_path = DataProvider::getWorkingDir() . "/.daemon_pid";
if (file_exists($pid_path)) {
unlink($pid_path);
}
} }
} }

View File

@@ -25,13 +25,12 @@ class OnStart implements SwooleEvent
if (!Framework::$argv["disable-safe-exit"]) { if (!Framework::$argv["disable-safe-exit"]) {
SignalListener::signalMaster($server); SignalListener::signalMaster($server);
} }
if (Framework::$argv["daemon"]) { $daemon_data = json_encode([
$daemon_data = json_encode([ "pid" => $server->master_pid,
"pid" => $server->master_pid, "stdout" => ZMConfig::get("global")["swoole"]["log_file"],
"stdout" => ZMConfig::get("global")["swoole"]["log_file"] "daemon" => (bool)Framework::$argv["daemon"]
], 128 | 256); ], 128 | 256);
file_put_contents(DataProvider::getWorkingDir() . "/.daemon_pid", $daemon_data); file_put_contents(DataProvider::getWorkingDir() . "/.daemon_pid", $daemon_data);
}
} }

View File

@@ -12,6 +12,7 @@ use Swoole\Database\PDOConfig;
use Swoole\Process; use Swoole\Process;
use Swoole\Server; use Swoole\Server;
use ZM\Annotation\AnnotationParser; use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnMessageEvent;
use ZM\Annotation\Swoole\OnStart; use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnSwooleEvent; use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Annotation\Swoole\SwooleHandler; use ZM\Annotation\Swoole\SwooleHandler;
@@ -24,7 +25,7 @@ use ZM\Event\EventDispatcher;
use ZM\Event\EventManager; use ZM\Event\EventManager;
use ZM\Event\SwooleEvent; use ZM\Event\SwooleEvent;
use ZM\Exception\DbException; use ZM\Exception\DbException;
use ZM\Exception\ZMException; use ZM\Exception\ZMKnownException;
use ZM\Framework; use ZM\Framework;
use ZM\Module\QQBot; use ZM\Module\QQBot;
use ZM\MySQL\MySQLPool; use ZM\MySQL\MySQLPool;
@@ -32,6 +33,7 @@ use ZM\Store\LightCacheInside;
use ZM\Store\MySQL\SqlPoolStorage; use ZM\Store\MySQL\SqlPoolStorage;
use ZM\Store\Redis\ZMRedisPool; use ZM\Store\Redis\ZMRedisPool;
use ZM\Utils\DataProvider; use ZM\Utils\DataProvider;
use ZM\Utils\Manager\ModuleManager;
use ZM\Utils\SignalListener; use ZM\Utils\SignalListener;
/** /**
@@ -48,7 +50,6 @@ class OnWorkerStart implements SwooleEvent
} }
unset(Context::$context[Coroutine::getCid()]); unset(Context::$context[Coroutine::getCid()]);
if ($server->taskworker === false) { if ($server->taskworker === false) {
zm_atomic("_#worker_" . $worker_id)->set($server->worker_pid); zm_atomic("_#worker_" . $worker_id)->set($server->worker_pid);
if (LightCacheInside::get("wait_api", "wait_api") !== null) { if (LightCacheInside::get("wait_api", "wait_api") !== null) {
LightCacheInside::unset("wait_api", "wait_api"); LightCacheInside::unset("wait_api", "wait_api");
@@ -131,7 +132,7 @@ class OnWorkerStart implements SwooleEvent
* @throws Exception * @throws Exception
*/ */
private function loadAnnotations() { private function loadAnnotations() {
if (Framework::$instant_mode) goto skip;
//加载各个模块的注解类,以及反射 //加载各个模块的注解类,以及反射
Console::debug("Mapping annotations"); Console::debug("Mapping annotations");
$parser = new AnnotationParser(); $parser = new AnnotationParser();
@@ -144,38 +145,48 @@ class OnWorkerStart implements SwooleEvent
$parser->addRegisterPath(DataProvider::getSourceRootDir() . "/" . $v . "/", trim($k, "\\")); $parser->addRegisterPath(DataProvider::getSourceRootDir() . "/" . $v . "/", trim($k, "\\"));
} }
} }
// 检查是否允许热加载phar模块允许的话将遍历phar内的文件
$plugin_enable_hotload = ZMConfig::get("global", "module_loader")["enable_hotload"] ?? false;
if ($plugin_enable_hotload) {
$list = ModuleManager::getPackedModules();
foreach($list as $k => $v) {
if (\server()->worker_id === 0) Console::info("Loading packed module: ".$k);
require_once $v["phar-path"];
$func = "loader".$v["generated-id"];
$func();
$parser->addRegisterPath("phar://".$v["phar-path"]."/".$v["module-root-path"], $v["namespace"]);
}
}
$parser->registerMods(); $parser->registerMods();
EventManager::loadEventByParser($parser); //加载事件 EventManager::loadEventByParser($parser); //加载事件
skip:
//加载自定义的全局函数 //加载自定义的全局函数
Console::debug("Loading context class..."); Console::debug("Loading context class...");
$context_class = ZMConfig::get("global", "context_class"); $context_class = ZMConfig::get("global", "context_class");
if (!is_a($context_class, ContextInterface::class, true)) { if (!is_a($context_class, ContextInterface::class, true)) {
throw new ZMException(zm_internal_errcode("E00032") . "Context class must implemented from ContextInterface!"); throw new ZMKnownException("E00032", "Context class must implemented from ContextInterface!");
} }
//加载插件 //加载插件
$obb_onebot = ZMConfig::get("global", "onebot") ?? $obb_onebot = ZMConfig::get("global", "onebot") ??
ZMConfig::get("global", "modules")["onebot"] ?? ZMConfig::get("global", "modules")["onebot"] ??
["status" => true, "single_bot_mode" => false, "message_level" => 99999]; ["status" => true, "single_bot_mode" => false, "message_level" => 99999];
if ($obb_onebot["status"]) { if ($obb_onebot["status"]) {
Console::debug("OneBot support enabled, listening OneBot event(3)."); Console::debug("OneBot support enabled, listening OneBot event(3).");
$obj = new OnSwooleEvent(); $obj = new OnMessageEvent();
$obj->class = QQBot::class; $obj->class = QQBot::class;
$obj->method = 'handleByEvent'; $obj->method = 'handleByEvent';
$obj->type = 'message'; $obj->level = $obb_onebot["message_level"] ?? 99;
$obj->level = $obb_onebot["message_level"] ?? 99999;
$obj->rule = 'connectIsQQ()'; $obj->rule = 'connectIsQQ()';
EventManager::addEvent(OnSwooleEvent::class, $obj); EventManager::addEvent(OnMessageEvent::class, $obj);
if ($obb_onebot["single_bot_mode"]) { if ($obb_onebot["single_bot_mode"]) {
LightCacheInside::set("connect", "conn_fd", -1); LightCacheInside::set("connect", "conn_fd", -1);
} else { } else {
LightCacheInside::set("connect", "conn_fd", -2); LightCacheInside::set("connect", "conn_fd", -2);
} }
} }
//TODO: 编写加载外部插件的方式
} }
private function initMySQLPool() { private function initMySQLPool() {
@@ -233,7 +244,7 @@ class OnWorkerStart implements SwooleEvent
->withPassword($real_conf["password"]) ->withPassword($real_conf["password"])
->withOptions($real_conf["options"] ?? [PDO::ATTR_STRINGIFY_FETCHES => false]) ->withOptions($real_conf["options"] ?? [PDO::ATTR_STRINGIFY_FETCHES => false])
); );
DB::initTableList(); DB::initTableList($real_conf["dbname"]);
} }
} }
} }

View File

@@ -0,0 +1,10 @@
<?php
namespace ZM\Exception;
class AnnotationException extends \Doctrine\Common\Annotations\AnnotationException
{
}

View File

@@ -6,9 +6,9 @@ namespace ZM\Exception;
use Throwable; use Throwable;
class DbException extends ZMException class DbException extends ZMKnownException
{ {
public function __construct($message = "", $code = 0, Throwable $previous = null) { public function __construct($message = "", $code = 0, Throwable $previous = null) {
parent::__construct(zm_internal_errcode("E00043") . $message, $code, $previous); parent::__construct("E00043", $message, $code, $previous);
} }
} }

View File

@@ -0,0 +1,14 @@
<?php
namespace ZM\Exception;
use Throwable;
class LightCacheException extends ZMKnownException
{
public function __construct($err_code, $message = "", $code = 0, Throwable $previous = null) {
parent::__construct($err_code, $message, $code, $previous);
}
}

View File

@@ -6,7 +6,7 @@ namespace ZM\Exception;
use Exception; use Exception;
class ZMException extends Exception abstract class ZMException extends Exception
{ {
} }

View File

@@ -0,0 +1,14 @@
<?php
namespace ZM\Exception;
use Throwable;
class ZMKnownException extends ZMException
{
public function __construct($err_code, $message = "", $code = 0, Throwable $previous = null) {
parent::__construct(zm_internal_errcode($err_code) . $message, $code, $previous);
}
}

View File

@@ -25,6 +25,33 @@ use ZM\Annotation\Swoole\SwooleHandler;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Utils\Terminal; use ZM\Utils\Terminal;
use ZM\Utils\ZMUtil; use ZM\Utils\ZMUtil;
use function date_default_timezone_set, define;
use function exec;
use function explode;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function get_class;
use function get_included_files;
use function in_array;
use function intval;
use function is_array;
use function is_dir;
use function json_decode;
use function json_encode;
use function mkdir;
use function ob_get_clean;
use function ob_start;
use function posix_getpid;
use function str_pad;
use function strlen;
use function substr;
use function swoole_cpu_num;
use function trim;
use function uuidgen;
use function working_dir;
use function zm_atomic;
use function zm_internal_errcode;
class Framework class Framework
{ {
@@ -49,9 +76,11 @@ class Framework
*/ */
private $setup_events = []; private $setup_events = [];
public function __construct($args = []) { public static $instant_mode = false;
$tty_width = $this->getTtyWidth();
public function __construct($args = [], $instant_mode = false) {
$tty_width = $this->getTtyWidth();
self::$instant_mode = $instant_mode;
self::$argv = $args; self::$argv = $args;
ZMConfig::setDirectory(DataProvider::getSourceRootDir() . '/config'); ZMConfig::setDirectory(DataProvider::getSourceRootDir() . '/config');
ZMConfig::setEnv($args["env"] ?? ""); ZMConfig::setEnv($args["env"] ?? "");
@@ -97,7 +126,9 @@ class Framework
$this->server_set["log_level"] = SWOOLE_LOG_DEBUG; $this->server_set["log_level"] = SWOOLE_LOG_DEBUG;
$add_port = ZMConfig::get("global", "remote_terminal")["status"] ?? false; $add_port = ZMConfig::get("global", "remote_terminal")["status"] ?? false;
$this->loadServerEvents(); if ($instant_mode) {
$this->loadServerEvents();
}
$this->parseCliArgs(self::$argv, $add_port); $this->parseCliArgs(self::$argv, $add_port);
@@ -121,7 +152,7 @@ class Framework
} else { } else {
$out["worker"] = $this->server_set["worker_num"]; $out["worker"] = $this->server_set["worker_num"];
} }
$out["environment"] = $args["env"] === null ? "default" : $args["env"]; $out["environment"] = ($args["env"] ?? null) === null ? "default" : $args["env"];
$out["log_level"] = Console::getLevel(); $out["log_level"] = Console::getLevel();
$out["version"] = ZM_VERSION . (LOAD_MODE == 0 ? (" (build " . ZM_VERSION_ID . ")") : ""); $out["version"] = ZM_VERSION . (LOAD_MODE == 0 ? (" (build " . ZM_VERSION_ID . ")") : "");
$out["master_pid"] = posix_getpid(); $out["master_pid"] = posix_getpid();
@@ -129,7 +160,7 @@ class Framework
if (isset($this->server_set["task_worker_num"])) { if (isset($this->server_set["task_worker_num"])) {
$out["task_worker"] = $this->server_set["task_worker_num"]; $out["task_worker"] = $this->server_set["task_worker_num"];
} }
if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") { if ((ZMConfig::get("global", "sql_config")["sql_host"] ?? "") !== "") {
$conf = ZMConfig::get("global", "sql_config"); $conf = ZMConfig::get("global", "sql_config");
$out["mysql_pool"] = $conf["sql_database"] . "@" . $conf["sql_host"] . ":" . $conf["sql_port"]; $out["mysql_pool"] = $conf["sql_database"] . "@" . $conf["sql_host"] . ":" . $conf["sql_port"];
} }
@@ -155,7 +186,7 @@ class Framework
} }
self::printProps($out, $tty_width, $args["log-theme"] === null); self::printProps($out, $tty_width, $args["log-theme"] === null);
if ($args["preview"]) { if ($args["preview"] ?? false) {
exit(); exit();
} }
@@ -305,9 +336,7 @@ class Framework
$motd = file_get_contents(__DIR__ . "/../../config/motd.txt"); $motd = file_get_contents(__DIR__ . "/../../config/motd.txt");
} }
$motd = explode("\n", $motd); $motd = explode("\n", $motd);
foreach ($motd as $k => $v) { foreach ($motd as $k => $v) $motd[$k] = substr($v, 0, $tty_width);
$motd[$k] = substr($v, 0, $tty_width);
}
$motd = implode("\n", $motd); $motd = implode("\n", $motd);
echo $motd; echo $motd;
} }

View File

@@ -0,0 +1,18 @@
<?php
namespace ZM\Module;
/**
* @since 2.6
*/
class InstantModule extends ModuleBase
{
public function onEvent($event_class, $params, callable $callable) {
$class = new $event_class();
foreach ($params as $k => $v) {
if (is_string($k)) $class->$k = $v;
}
$class->method = $callable;
$this->events[] = $class;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace ZM\Module;
/**
* @since 2.6
*/
abstract class ModuleBase
{
protected $module_name;
protected $events = [];
public function __construct($module_name) {
$this->module_name = $module_name;
}
/**
* @return mixed
*/
public function getModuleName() {
return $this->module_name;
}
/**
* @return array
*/
public function getEvents(): array {
return $this->events;
}
}

View File

@@ -54,6 +54,7 @@ class ModulePacker
*/ */
public function setOutputPath($path) { public function setOutputPath($path) {
$this->output_path = $path; $this->output_path = $path;
if (!is_dir($path)) mkdir($path, 0755, true);
} }
/** /**
@@ -122,7 +123,13 @@ class ModulePacker
} }
private function generatePharAutoload(): array { private function generatePharAutoload(): array {
return ZMUtil::getClassesPsr4($this->module['module-path'], $this->module['namespace'], null, true); $pos = strpos($this->module['module-path'], DataProvider::getSourceRootDir().'/');
if ($pos === 0) {
$path_value = substr($this->module['module-path'], strlen(DataProvider::getSourceRootDir().'/'));
} else {
throw new ModulePackException(zm_internal_errcode("E99999")); //未定义的错误
}
return ZMUtil::getClassesPsr4($this->module['module-path'], $this->module['namespace'], null, $path_value);
} }
private function getComposerAutoloadItems(): array { private function getComposerAutoloadItems(): array {

View File

@@ -32,13 +32,14 @@ class ModuleUnpacker
* @param bool $override_light_cache * @param bool $override_light_cache
* @param bool $override_data_files * @param bool $override_data_files
* @param bool $override_source * @param bool $override_source
* @param bool $ignore_depends
* @return array * @return array
* @throws ModulePackException * @throws ModulePackException
* @throws ZMException * @throws ZMException
*/ */
public function unpack(bool $override_light_cache = false, bool $override_data_files = false, bool $override_source = false): array { public function unpack(bool $override_light_cache = false, bool $override_data_files = false, bool $override_source = false, $ignore_depends = false): array {
$this->checkConfig(); $this->checkConfig();
$this->checkDepends(); $this->checkDepends($ignore_depends);
$this->checkLightCacheStore(); $this->checkLightCacheStore();
$this->checkZMDataStore(); $this->checkZMDataStore();
@@ -62,18 +63,19 @@ class ModuleUnpacker
/** /**
* 检查模块依赖关系 * 检查模块依赖关系
* @param bool $ignore_depends
* @throws ModulePackException * @throws ModulePackException
* @throws ZMException * @throws ZMException
*/ */
private function checkDepends() { private function checkDepends($ignore_depends = false) {
$configured = ModuleManager::getConfiguredModules(); $configured = ModuleManager::getConfiguredModules();
$depends = $this->module_config["depends"] ?? []; $depends = $this->module_config["depends"] ?? [];
foreach ($depends as $k => $v) { foreach ($depends as $k => $v) {
if (!isset($configured[$k])) { if (!isset($configured[$k]) && !$ignore_depends) {
throw new ModulePackException(zm_internal_errcode("E00064") . "模块 " . $this->module_config["name"] . " 依赖的模块 $k 不存在"); throw new ModulePackException(zm_internal_errcode("E00064") . "模块 " . $this->module_config["name"] . " 依赖的模块 $k 不存在");
} }
$current_ver = $configured[$k]["version"] ?? "1.0"; $current_ver = $configured[$k]["version"] ?? "1.0";
if (!VersionComparator::compareVersionRange($current_ver, $v)) { if (!VersionComparator::compareVersionRange($current_ver, $v) && !$ignore_depends) {
throw new ModulePackException(zm_internal_errcode("E00063") . "模块 " . $this->module_config["name"] . " 依赖的模块 $k 版本依赖不符合条件(现有版本: " . $current_ver . ", 需求版本: " . $v . ""); throw new ModulePackException(zm_internal_errcode("E00063") . "模块 " . $this->module_config["name"] . " 依赖的模块 $k 版本依赖不符合条件(现有版本: " . $current_ver . ", 需求版本: " . $v . "");
} }
} }

View File

@@ -73,11 +73,11 @@ class QQBot
public function dispatchBeforeEvents($data, $time): EventDispatcher { public function dispatchBeforeEvents($data, $time): EventDispatcher {
$before = new EventDispatcher(CQBefore::class); $before = new EventDispatcher(CQBefore::class);
if ($time === "pre") { if ($time === "pre") {
$before->setRuleFunction(function($v) use ($data){ $before->setRuleFunction(function ($v) use ($data) {
return $v->level >= 200 && $v->cq_event == $data["post_type"]; return $v->level >= 200 && $v->cq_event == $data["post_type"];
}); });
} else { } else {
$before->setRuleFunction(function($v) use ($data){ $before->setRuleFunction(function ($v) use ($data) {
return $v->level < 200 && $v->cq_event == $data["post_type"]; return $v->level < 200 && $v->cq_event == $data["post_type"];
}); });
} }
@@ -103,7 +103,7 @@ class QQBot
if (is_string($result)) ctx()->reply($result); if (is_string($result)) ctx()->reply($result);
if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt(); if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt();
}); });
$s = MessageUtil::matchCommand(ctx()->getMessage(), ctx()->getData()); $s = MessageUtil::matchCommand(ctx()->getStringMessage(), ctx()->getData());
if ($s->status !== false) { if ($s->status !== false) {
if (!empty($s->match)) ctx()->setCache("match", $s->match); if (!empty($s->match)) ctx()->setCache("match", $s->match);
$dispatcher->dispatchEvent($s->object, null); $dispatcher->dispatchEvent($s->object, null);
@@ -114,11 +114,11 @@ class QQBot
//分发CQMessage事件 //分发CQMessage事件
$msg_dispatcher = new EventDispatcher(CQMessage::class); $msg_dispatcher = new EventDispatcher(CQMessage::class);
$msg_dispatcher->setRuleFunction(function ($v) { $msg_dispatcher->setRuleFunction(function ($v) {
return ($v->message == '' || ($v->message != '' && $v->message == context()->getData()["message"])) && return ($v->message == '' || ($v->message == ctx()->getStringMessage())) &&
($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == context()->getData()["user_id"])) && ($v->user_id == 0 || ($v->user_id == ctx()->getUserId())) &&
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (context()->getData()["group_id"] ?? 0))) && ($v->group_id == 0 || ($v->group_id == (ctx()->getGroupId() ?? 0))) &&
($v->message_type == '' || ($v->message_type != '' && $v->message_type == context()->getData()["message_type"])) && ($v->message_type == '' || ($v->message_type == ctx()->getMessageType())) &&
($v->raw_message == '' || ($v->raw_message != '' && $v->raw_message == context()->getData()["raw_message"])); ($v->raw_message == '' || ($v->raw_message == context()->getData()["raw_message"]));
}); });
$msg_dispatcher->setReturnFunction(function ($result) { $msg_dispatcher->setReturnFunction(function ($result) {
if (is_string($result)) ctx()->reply($result); if (is_string($result)) ctx()->reply($result);

View File

@@ -21,12 +21,13 @@ class MySQLConnection implements Connection
private $conn; private $conn;
public function __construct() { public function __construct() {
Console::info("Constructing..."); Console::debug("Constructing...");
$this->conn = SqlPoolStorage::$sql_pool->getConnection(); $this->conn = SqlPoolStorage::$sql_pool->getConnection();
} }
public function prepare($sql, $options = []) { public function prepare($sql, $options = []) {
try { try {
Console::debug("Running SQL prepare: ".$sql);
$statement = $this->conn->prepare($sql, $options); $statement = $this->conn->prepare($sql, $options);
assert(($statement instanceof PDOStatementProxy) || ($statement instanceof PDOStatement)); assert(($statement instanceof PDOStatementProxy) || ($statement instanceof PDOStatement));
} catch (PDOException $exception) { } catch (PDOException $exception) {
@@ -51,6 +52,7 @@ class MySQLConnection implements Connection
public function exec($sql) { public function exec($sql) {
try { try {
Console::debug("Running SQL exec: ".$sql);
$statement = $this->conn->exec($sql); $statement = $this->conn->exec($sql);
assert($statement !== false); assert($statement !== false);
return $statement; return $statement;
@@ -88,7 +90,7 @@ class MySQLConnection implements Connection
} }
public function __destruct() { public function __destruct() {
Console::info("Destructing"); Console::debug("Destructing");
SqlPoolStorage::$sql_pool->putConnection($this->conn); SqlPoolStorage::$sql_pool->putConnection($this->conn);
} }
} }

View File

@@ -4,16 +4,16 @@
namespace ZM\MySQL; namespace ZM\MySQL;
use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver as DoctrineDriver;
use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Schema\MySqlSchemaManager; use Doctrine\DBAL\Schema\MySqlSchemaManager;
use ZM\Config\ZMConfig; use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
class MySQLDriver implements Driver class MySQLDriver implements DoctrineDriver
{ {
public function connect(array $params, $username = null, $password = null, array $driverOptions = []) { public function connect(array $params, $username = null, $password = null, array $driverOptions = []) {
Console::info("Requiring new connection"); Console::debug("Requiring new connection");
return new MySQLConnection(); return new MySQLConnection();
} }

View File

@@ -4,17 +4,12 @@
namespace ZM\MySQL; namespace ZM\MySQL;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
class MySQLManager class MySQLManager
{ {
/** /**
* @return Connection * @return MySQLWrapper
* @throws Exception
*/ */
public static function getConnection() { public static function getWrapper(): MySQLWrapper {
return DriverManager::getConnection(["driverClass" => MySQLDriver::class]); return new MySQLWrapper();
} }
} }

View File

@@ -34,4 +34,8 @@ class MySQLPool extends PDOPool
$this->count--; $this->count--;
parent::put($connection); parent::put($connection);
} }
public function getCount() {
return $this->count;
}
} }

View File

@@ -0,0 +1,29 @@
<?php
namespace ZM\MySQL;
use Doctrine\DBAL\Query\QueryBuilder;
use ZM\Exception\DbException;
class MySQLQueryBuilder extends QueryBuilder
{
private $wrapper;
public function __construct(MySQLWrapper $wrapper) {
parent::__construct($wrapper->getConnection());
$this->wrapper = $wrapper;
}
/**
* @return int|MySQLStatementWrapper
* @throws DbException
*/
public function execute() {
if ($this->getType() === self::SELECT) {
return $this->wrapper->executeQuery($this->getSQL(), $this->getParameters(), $this->getParameterTypes());
}
return $this->wrapper->executeStatement($this->getSQL(), $this->getParameters(), $this->getParameterTypes());
}
}

View File

@@ -7,11 +7,12 @@ namespace ZM\MySQL;
use Doctrine\DBAL\Driver\Statement; use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\Driver\StatementIterator; use Doctrine\DBAL\Driver\StatementIterator;
use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\ParameterType;
use IteratorAggregate;
use PDO; use PDO;
use PDOStatement; use PDOStatement;
use Swoole\Database\PDOStatementProxy; use Swoole\Database\PDOStatementProxy;
class MySQLStatement implements Statement, \IteratorAggregate class MySQLStatement implements IteratorAggregate, Statement
{ {
/** @var PDOStatement|PDOStatementProxy */ /** @var PDOStatement|PDOStatementProxy */
private $statement; private $statement;
@@ -29,7 +30,14 @@ class MySQLStatement implements Statement, \IteratorAggregate
} }
public function setFetchMode($fetchMode, $arg2 = null, $arg3 = []) { public function setFetchMode($fetchMode, $arg2 = null, $arg3 = []) {
return $this->statement->setFetchMode($fetchMode, $arg2, $arg3); if ($arg2 !== null && $arg3 !== [])
return $this->statement->setFetchMode($fetchMode, $arg2, $arg3);
elseif ($arg2 !== null && $arg3 === [])
return $this->statement->setFetchMode($fetchMode, $arg2);
elseif ($arg2 === null && $arg3 !== [])
return $this->statement->setFetchMode($fetchMode, $arg2, $arg3);
else
return $this->statement->setFetchMode($fetchMode);
} }
public function fetch($fetchMode = PDO::FETCH_ASSOC, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) { public function fetch($fetchMode = PDO::FETCH_ASSOC, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) {

View File

@@ -0,0 +1,232 @@
<?php
/**
* @noinspection PhpMissingReturnTypeInspection
* @noinspection PhpUnused
*/
namespace ZM\MySQL;
use Doctrine\DBAL\Driver\ResultStatement;
use Doctrine\DBAL\ForwardCompatibility\Result;
use Throwable;
use Traversable;
use ZM\Exception\DbException;
class MySQLStatementWrapper
{
public $stmt;
public function __construct(?Result $stmt) {
$this->stmt = $stmt;
}
/**
* 获取结果的迭代器
* wrapper method
* @return ResultStatement
*/
public function getIterator() {
return $this->stmt->getIterator();
}
/**
* 获取列数
* wrapper method
* @return int
*/
public function columnCount() {
return $this->stmt->columnCount();
}
/**
* wrapper method
* @return array|false|mixed
* @throws DbException
*/
public function fetchNumeric() {
try {
return $this->stmt->fetchNumeric();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return array|false|mixed
* @throws DbException
*/
public function fetchAssociative() {
try {
return $this->stmt->fetchAssociative();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return false|mixed
* @throws DbException
*/
public function fetchOne() {
try {
return $this->stmt->fetchOne();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return array
* @throws DbException
*/
public function fetchAllNumeric(): array {
try {
return $this->stmt->fetchAllNumeric();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return array
* @throws DbException
*/
public function fetchAllAssociative(): array {
try {
return $this->stmt->fetchAllAssociative();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return array
* @throws DbException
*/
public function fetchAllKeyValue(): array {
try {
return $this->stmt->fetchAllKeyValue();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return array
* @throws DbException
*/
public function fetchAllAssociativeIndexed(): array {
try {
return $this->stmt->fetchAllAssociativeIndexed();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return array
* @throws DbException
*/
public function fetchFirstColumn(): array {
try {
return $this->stmt->fetchFirstColumn();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return Traversable
* @throws DbException
*/
public function iterateNumeric(): Traversable {
try {
return $this->stmt->iterateNumeric();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return Traversable
* @throws DbException
*/
public function iterateAssociative(): Traversable {
try {
return $this->stmt->iterateAssociative();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return Traversable
* @throws DbException
*/
public function iterateKeyValue(): Traversable {
try {
return $this->stmt->iterateKeyValue();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return Traversable
* @throws DbException
*/
public function iterateAssociativeIndexed(): Traversable {
try {
return $this->stmt->iterateAssociativeIndexed();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return Traversable
* @throws DbException
*/
public function iterateColumn(): Traversable {
try {
return $this->stmt->iterateColumn();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return int
* @throws DbException
*/
public function rowCount() {
try {
return $this->stmt->rowCount();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
*/
public function free(): void {
$this->stmt->free();
}
}

View File

@@ -1,15 +1,575 @@
<?php <?php
/**
* @noinspection PhpUnused
*/
namespace ZM\MySQL; namespace ZM\MySQL;
use Closure;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\ParameterType;
use Throwable;
use Traversable;
use ZM\Exception\DbException;
class MySQLWrapper class MySQLWrapper
{ {
public $connection; private $connection;
/**
* MySQLWrapper constructor.
* @throws DbException
*/
public function __construct() { public function __construct() {
$this->connection = MySQLManager::getConnection(); try {
$this->connection = DriverManager::getConnection(["driverClass" => MySQLDriver::class]);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return string
*/
public function getDatabase(): string {
return $this->connection->getDatabase();
}
/**
* wrapper method
* @return bool
*/
public function isAutoCommit(): bool {
return $this->connection->isAutoCommit();
}
/**
* wrapper method
* @param $autoCommit
*/
public function setAutoCommit($autoCommit) {
$this->connection->setAutoCommit($autoCommit);
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return array|false
* @throws DbException
*/
public function fetchAssociative(string $query, array $params = [], array $types = []) {
try {
return $this->connection->fetchAssociative($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return array|false
* @throws DbException
*/
public function fetchNumeric(string $query, array $params = [], array $types = []) {
try {
return $this->connection->fetchNumeric($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* @param string $query
* @param array $params
* @param array $types
* @return false|mixed
* @throws DbException
*/
public function fetchOne(string $query, array $params = [], array $types = []): bool {
try {
return $this->connection->fetchOne($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return bool
*/
public function isTransactionActive(): bool {
return $this->connection->isTransactionActive();
}
/**
* @param $table
* @param array $criteria
* @param array $types
* @return int
* @throws DbException
*/
public function delete($table, array $criteria, array $types = []): int {
try {
return $this->connection->delete($table, $criteria, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $level
* @return int
*/
public function setTransactionIsolation($level): int {
return $this->connection->setTransactionIsolation($level);
}
/**
* wrapper method
* @return int|null
*/
public function getTransactionIsolation(): ?int {
return $this->connection->getTransactionIsolation();
}
/**
* wrapper method
* @param $table
* @param array $data
* @param array $criteria
* @param array $types
* @return int
* @throws DbException
*/
public function update($table, array $data, array $criteria, array $types = []): int {
try {
return $this->connection->update($table, $data, $criteria, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $table
* @param array $data
* @param array $types
* @return int
* @throws DbException
*/
public function insert($table, array $data, array $types = []): int {
try {
return $this->connection->insert($table, $data, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $str
* @return string
*/
public function quoteIdentifier($str): string {
return $this->connection->quoteIdentifier($str);
}
/**
* wrapper method
* @param $value
* @param int $type
* @return mixed
*/
public function quote($value, $type = ParameterType::STRING) {
return $this->connection->quote($value, $type);
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return array
* @throws DbException
*/
public function fetchAllNumeric(string $query, array $params = [], array $types = []): array {
try {
return $this->connection->fetchAllNumeric($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return array
* @throws DbException
*/
public function fetchAllAssociative(string $query, array $params = [], array $types = []): array {
try {
return $this->connection->fetchAllAssociative($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return array
* @throws DbException
*/
public function fetchAllKeyValue(string $query, array $params = [], array $types = []): array {
try {
return $this->connection->fetchAllKeyValue($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return array
* @throws DbException
*/
public function fetchAllAssociativeIndexed(string $query, array $params = [], array $types = []): array {
try {
return $this->connection->fetchAllAssociativeIndexed($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return array
* @throws DbException
*/
public function fetchFirstColumn(string $query, array $params = [], array $types = []): array {
try {
return $this->connection->fetchFirstColumn($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return Traversable
* @throws DbException
*/
public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable {
try {
return $this->connection->iterateNumeric($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return Traversable
* @throws DbException
*/
public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable {
try {
return $this->connection->iterateAssociative($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return Traversable
* @throws DbException
*/
public function iterateKeyValue(string $query, array $params = [], array $types = []): Traversable {
try {
return $this->connection->iterateKeyValue($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return Traversable
* @throws DbException
*/
public function iterateAssociativeIndexed(string $query, array $params = [], array $types = []): Traversable {
try {
return $this->connection->iterateAssociativeIndexed($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param string $query
* @param array $params
* @param array $types
* @return Traversable
* @throws DbException
*/
public function iterateColumn(string $query, array $params = [], array $types = []): Traversable {
try {
return $this->connection->iterateColumn($query, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $sql
* @param array $params
* @param array $types
* @param QueryCacheProfile|null $qcp
* @return MySQLStatementWrapper
* @throws DbException
*/
public function executeQuery($sql, array $params = [], $types = [], ?QueryCacheProfile $qcp = null): MySQLStatementWrapper {
try {
$query = $this->connection->executeQuery($sql, $params, $types, $qcp);
return new MySQLStatementWrapper($query);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $sql
* @param $params
* @param $types
* @param QueryCacheProfile $qcp
* @return MySQLStatementWrapper
* @throws DbException
*/
public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): MySQLStatementWrapper {
try {
$query = $this->connection->executeCacheQuery($sql, $params, $types, $qcp);
return new MySQLStatementWrapper($query);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $sql
* @param array $params
* @param array $types
* @return int
* @throws DbException
*/
public function executeStatement($sql, array $params = [], array $types = []): int {
try {
return $this->connection->executeStatement($sql, $params, $types);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return int
*/
public function getTransactionNestingLevel(): int {
return $this->connection->getTransactionNestingLevel();
}
/**
* wrapper method
* @param null $name
* @return string
*/
public function lastInsertId($name = null): string {
return $this->connection->lastInsertId($name);
}
/**
* overwrite method to $this->connection->transactional()
* @param Closure $func
* @return mixed
* @throws DbException
*/
public function transactional(Closure $func) {
$this->beginTransaction();
try {
$res = $func($this);
$this->commit();
return $res;
} catch (Throwable $e) {
$this->rollBack();
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $nestTransactionsWithSavepoints
* @throws DbException
*/
public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints) {
try {
$this->connection->setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return bool
*/
public function getNestTransactionsWithSavepoints(): bool {
return $this->connection->getNestTransactionsWithSavepoints();
}
/**
* wrapper method
* @return bool
*/
public function beginTransaction(): bool {
return $this->connection->beginTransaction();
}
/**
* wrapper method
* @return bool
* @throws DbException
*/
public function commit(): bool {
try {
return $this->connection->commit();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return bool
* @throws DbException
*/
public function rollBack(): bool {
try {
return $this->connection->rollBack();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $savepoint
* @throws DbException
*/
public function createSavepoint($savepoint) {
try {
$this->connection->createSavepoint($savepoint);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $savepoint
* @throws DbException
*/
public function releaseSavepoint($savepoint) {
try {
$this->connection->releaseSavepoint($savepoint);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @param $savepoint
* @throws DbException
*/
public function rollbackSavepoint($savepoint) {
try {
$this->connection->rollbackSavepoint($savepoint);
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @throws DbException
*/
public function setRollbackOnly() {
try {
$this->connection->setRollbackOnly();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* wrapper method
* @return bool
* @throws DbException
*/
public function isRollbackOnly(): bool {
try {
return $this->connection->isRollbackOnly();
} catch (Throwable $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* overwrite method to $this->connection->createQueryBuilder
* @return MySQLQueryBuilder
*/
public function createQueryBuilder(): MySQLQueryBuilder {
return new MySQLQueryBuilder($this);
}
public function getConnection(): Connection {
return $this->connection;
} }
public function __destruct() { public function __destruct() {

View File

@@ -9,6 +9,7 @@ use Swoole\Table;
use ZM\Annotation\Swoole\OnSave; use ZM\Annotation\Swoole\OnSave;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Event\EventDispatcher; use ZM\Event\EventDispatcher;
use ZM\Exception\LightCacheException;
use ZM\Exception\ZMException; use ZM\Exception\ZMException;
use ZM\Framework; use ZM\Framework;
use ZM\Utils\Manager\ProcessManager; use ZM\Utils\Manager\ProcessManager;
@@ -86,7 +87,7 @@ class LightCache
* @throws ZMException * @throws ZMException
*/ */
public static function get(string $key) { public static function get(string $key) {
if (self::$kv_table === null) throw new ZMException(zm_internal_errcode("E00048") . "not initialized LightCache"); if (self::$kv_table === null) throw new LightCacheException("E00048", "not initialized LightCache");
self::checkExpire($key); self::checkExpire($key);
$r = self::$kv_table->get($key); $r = self::$kv_table->get($key);
return $r === false ? null : self::parseGet($r); return $r === false ? null : self::parseGet($r);
@@ -98,7 +99,7 @@ class LightCache
* @throws ZMException * @throws ZMException
*/ */
public static function getExpire(string $key) { public static function getExpire(string $key) {
if (self::$kv_table === null) throw new ZMException(zm_internal_errcode("E00048") . "not initialized LightCache"); if (self::$kv_table === null) throw new LightCacheException("E00048", "not initialized LightCache");
self::checkExpire($key); self::checkExpire($key);
$r = self::$kv_table->get($key, "expire"); $r = self::$kv_table->get($key, "expire");
return $r === false ? null : $r - time(); return $r === false ? null : $r - time();
@@ -111,7 +112,7 @@ class LightCache
* @since 2.4.3 * @since 2.4.3
*/ */
public static function getExpireTS(string $key) { public static function getExpireTS(string $key) {
if (self::$kv_table === null) throw new ZMException(zm_internal_errcode("E00048") . "not initialized LightCache"); if (self::$kv_table === null) throw new LightCacheException("E00048", "not initialized LightCache");
self::checkExpire($key); self::checkExpire($key);
$r = self::$kv_table->get($key, "expire"); $r = self::$kv_table->get($key, "expire");
return $r === false ? null : $r; return $r === false ? null : $r;
@@ -125,7 +126,7 @@ class LightCache
* @throws ZMException * @throws ZMException
*/ */
public static function set(string $key, $value, int $expire = -1) { public static function set(string $key, $value, int $expire = -1) {
if (self::$kv_table === null) throw new ZMException(zm_internal_errcode("E00048") . "not initialized LightCache"); if (self::$kv_table === null) throw new LightCacheException("E00048", "not initialized LightCache");
if (is_array($value)) { if (is_array($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE); $value = json_encode($value, JSON_UNESCAPED_UNICODE);
if (strlen($value) >= self::$config["max_strlen"]) return false; if (strlen($value) >= self::$config["max_strlen"]) return false;
@@ -138,7 +139,7 @@ class LightCache
$data_type = "bool"; $data_type = "bool";
$value = json_encode($value); $value = json_encode($value);
} else { } else {
throw new ZMException(zm_internal_errcode("E00049") . "Only can set string, array and int"); throw new LightCacheException("E00049", "Only can set string, array and int");
} }
try { try {
return self::$kv_table->set($key, [ return self::$kv_table->set($key, [
@@ -158,7 +159,7 @@ class LightCache
* @throws ZMException * @throws ZMException
*/ */
public static function update(string $key, $value) { public static function update(string $key, $value) {
if (self::$kv_table === null) throw new ZMException(zm_internal_errcode("E00048") . "not initialized LightCache."); if (self::$kv_table === null) throw new LightCacheException("E00048", "not initialized LightCache.");
if (is_array($value)) { if (is_array($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE); $value = json_encode($value, JSON_UNESCAPED_UNICODE);
if (strlen($value) >= self::$config["max_strlen"]) return false; if (strlen($value) >= self::$config["max_strlen"]) return false;
@@ -168,7 +169,7 @@ class LightCache
} elseif (is_int($value)) { } elseif (is_int($value)) {
$data_type = "int"; $data_type = "int";
} else { } else {
throw new ZMException(zm_internal_errcode("E00048") . "Only can set string, array and int"); throw new LightCacheException("E00048", "Only can set string, array and int");
} }
try { try {
if (self::$kv_table->get($key) === false) return false; if (self::$kv_table->get($key) === false) return false;

View File

@@ -6,6 +6,7 @@ namespace ZM\Store;
use Exception; use Exception;
use Swoole\Table; use Swoole\Table;
use ZM\Exception\LightCacheException;
use ZM\Exception\ZMException; use ZM\Exception\ZMException;
class LightCacheInside class LightCacheInside
@@ -67,6 +68,6 @@ class LightCacheInside
self::$kv_table[$name] = new Table($size, $conflict_proportion); self::$kv_table[$name] = new Table($size, $conflict_proportion);
self::$kv_table[$name]->column("value", Table::TYPE_STRING, $str_size); self::$kv_table[$name]->column("value", Table::TYPE_STRING, $str_size);
$r = self::$kv_table[$name]->create(); $r = self::$kv_table[$name]->create();
if ($r === false) throw new ZMException(zm_internal_errcode("E00050") . "内存不足,创建静态表失败!"); if ($r === false) throw new LightCacheException("E00050", "内存不足,创建静态表失败!");
} }
} }

View File

@@ -32,6 +32,7 @@ class ZMAtomic
self::$atomics["wait_msg_id"] = new Atomic(0); self::$atomics["wait_msg_id"] = new Atomic(0);
self::$atomics["_event_id"] = new Atomic(0); self::$atomics["_event_id"] = new Atomic(0);
self::$atomics["server_is_stopped"] = new Atomic(0); self::$atomics["server_is_stopped"] = new Atomic(0);
if (!defined("ZM_WORKER_NUM")) define("ZM_WORKER_NUM", 1);
for($i = 0; $i < ZM_WORKER_NUM; ++$i) { for($i = 0; $i < ZM_WORKER_NUM; ++$i) {
self::$atomics["_#worker_".$i] = new Atomic(0); self::$atomics["_#worker_".$i] = new Atomic(0);
} }

0
src/ZM/Store/ZMBuf.php Executable file → Normal file
View File

View File

@@ -4,9 +4,11 @@
namespace ZM\Utils\Manager; namespace ZM\Utils\Manager;
use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Exception\ModulePackException; use ZM\Exception\ModulePackException;
use ZM\Exception\ZMException; use ZM\Exception\ZMException;
use ZM\Exception\ZMKnownException;
use ZM\Module\ModulePacker; use ZM\Module\ModulePacker;
use ZM\Module\ModuleUnpacker; use ZM\Module\ModuleUnpacker;
use ZM\Utils\DataProvider; use ZM\Utils\DataProvider;
@@ -36,12 +38,12 @@ class ModuleManager
if ($json === null) continue; if ($json === null) continue;
if (!isset($json["name"])) continue; if (!isset($json["name"])) continue;
if ($pathinfo["dirname"] == ".") { if ($pathinfo["dirname"] == ".") {
throw new ZMException(zm_internal_errcode("E00052") . "在/src/目录下不可以直接标记为模块(zm.json),因为命名空间不能为根空间!"); throw new ZMKnownException("E00052", "在/src/目录下不可以直接标记为模块(zm.json),因为命名空间不能为根空间!");
} }
$json["module-path"] = realpath($dir . "/" . $pathinfo["dirname"]); $json["module-path"] = realpath($dir . "/" . $pathinfo["dirname"]);
$json["namespace"] = str_replace("/", "\\", $pathinfo["dirname"]); $json["namespace"] = str_replace("/", "\\", $pathinfo["dirname"]);
if (isset($modules[$json["name"]])) { if (isset($modules[$json["name"]])) {
throw new ZMException(zm_internal_errcode("E00053") . "重名模块:" . $json["name"]); throw new ZMKnownException("E00053", "重名模块:" . $json["name"]);
} }
$modules[$json["name"]] = $json; $modules[$json["name"]] = $json;
} }
@@ -50,7 +52,7 @@ class ModuleManager
} }
public static function getPackedModules(): array { public static function getPackedModules(): array {
$dir = DataProvider::getDataFolder() . "modules"; $dir = ZMConfig::get("global", "module_loader")["load_path"] ?? (ZM_DATA . "modules");
$ls = DataProvider::scanDirFiles($dir, true, false); $ls = DataProvider::scanDirFiles($dir, true, false);
if ($ls === false) return []; if ($ls === false) return [];
$modules = []; $modules = [];
@@ -88,7 +90,10 @@ class ModuleManager
public static function packModule($module): bool { public static function packModule($module): bool {
try { try {
$packer = new ModulePacker($module); $packer = new ModulePacker($module);
$packer->setOutputPath(DataProvider::getDataFolder() . "output"); if (!is_dir(DataProvider::getDataFolder())) throw new ModulePackException(zm_internal_errcode("E00070") . "zm_data dir not found!");
$path = realpath(DataProvider::getDataFolder() . "/output");
if ($path === false) mkdir($path = DataProvider::getDataFolder() . "/output");
$packer->setOutputPath($path);
$packer->setOverride(); $packer->setOverride();
$packer->pack(); $packer->pack();
return true; return true;
@@ -107,7 +112,7 @@ class ModuleManager
public static function unpackModule($module, array $options = []) { public static function unpackModule($module, array $options = []) {
try { try {
$packer = new ModuleUnpacker($module); $packer = new ModuleUnpacker($module);
return $packer->unpack((bool)$options["override-light-cache"], (bool)$options["override-zm-data"], (bool)$options["override-source"]); return $packer->unpack((bool)$options["overwrite-light-cache"], (bool)$options["overwrite-zm-data"], (bool)$options["overwrite-source"], (bool)$options["ignore-depends"]);
} catch (ZMException $e) { } catch (ZMException $e) {
Console::error($e->getMessage()); Console::error($e->getMessage());
return false; return false;

View File

@@ -23,7 +23,7 @@ class MessageUtil
*/ */
public static function downloadCQImage($msg, $path = null) { public static function downloadCQImage($msg, $path = null) {
$path = $path ?? DataProvider::getDataFolder() . "images/"; $path = $path ?? DataProvider::getDataFolder() . "images/";
if (!is_dir($path)) mkdir($path); if (!is_dir($path)) @mkdir($path);
$path = realpath($path); $path = realpath($path);
if ($path === false) { if ($path === false) {
Console::warning(zm_internal_errcode("E00059") . "指定的路径错误不存在!"); Console::warning(zm_internal_errcode("E00059") . "指定的路径错误不存在!");
@@ -59,7 +59,7 @@ class MessageUtil
return false; return false;
} }
public static function isAtMe($msg, $me_id) { public static function isAtMe($msg, $me_id): bool {
return strpos($msg, CQ::at($me_id)) !== false; return strpos($msg, CQ::at($me_id)) !== false;
} }
@@ -72,7 +72,7 @@ class MessageUtil
* @param int $type * @param int $type
* @return string * @return string
*/ */
public static function getImageCQFromLocal($file, $type = 0): string { public static function getImageCQFromLocal($file, int $type = 0): string {
switch ($type) { switch ($type) {
case 0: case 0:
return CQ::image("base64://" . base64_encode(file_get_contents($file))); return CQ::image("base64://" . base64_encode(file_get_contents($file)));
@@ -90,7 +90,7 @@ class MessageUtil
* @param $msg * @param $msg
* @return array|string[] * @return array|string[]
*/ */
public static function splitCommand($msg) { public static function splitCommand($msg): array {
$word = explodeMsg(str_replace("\r", "", $msg)); $word = explodeMsg(str_replace("\r", "", $msg));
if (empty($word)) $word = [""]; if (empty($word)) $word = [""];
if (count(explode("\n", $word[0])) >= 2) { if (count(explode("\n", $word[0])) >= 2) {
@@ -98,7 +98,7 @@ class MessageUtil
$first = split_explode(" ", array_shift($enter)); $first = split_explode(" ", array_shift($enter));
$word = array_merge($first, $enter); $word = array_merge($first, $enter);
foreach ($word as $k => $v) { foreach ($word as $k => $v) {
$word[$k] = trim($word[$k]); $word[$k] = trim($v);
} }
} }
return $word; return $word;
@@ -109,15 +109,18 @@ class MessageUtil
* @param $obj * @param $obj
* @return MatchResult * @return MatchResult
*/ */
public static function matchCommand($msg, $obj) { public static function matchCommand($msg, $obj): MatchResult {
$ls = EventManager::$events[CQCommand::class] ?? []; $ls = EventManager::$events[CQCommand::class] ?? [];
if (is_array($msg)) {
$msg = self::arrayToStr($msg);
}
$word = self::splitCommand($msg); $word = self::splitCommand($msg);
$matched = new MatchResult(); $matched = new MatchResult();
foreach ($ls as $v) { foreach ($ls as $v) {
if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) continue; if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) continue;
elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == $obj["user_id"])) && elseif (($v->user_id == 0 || ($v->user_id == $obj["user_id"])) &&
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == ($obj["group_id"] ?? 0))) && ($v->group_id == 0 || ($v->group_id == ($obj["group_id"] ?? 0))) &&
($v->message_type == '' || ($v->message_type != '' && $v->message_type == $obj["message_type"])) ($v->message_type == '' || ($v->message_type == $obj["message_type"]))
) { ) {
if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) { if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) {
array_shift($word); array_shift($word);
@@ -166,4 +169,61 @@ class MessageUtil
ProcessManager::sendActionToWorker($i, "add_short_command", [$command, $reply]); ProcessManager::sendActionToWorker($i, "add_short_command", [$command, $reply]);
} }
} }
/**
* 字符串转数组
* @param $msg
* @param bool $ignore_space
* @param false $trim_text
* @return array
*/
public static function strToArray($msg, bool $ignore_space = true, bool $trim_text = false): array {
$arr = [];
while (($rear = mb_strstr($msg, '[CQ:')) !== false && ($end = mb_strstr($rear, ']', true)) !== false) {
// 把 [CQ: 前面的文字生成段落
$front = mb_strstr($msg, '[CQ:', true);
// 如果去掉空格都还有文字,或者不去掉空格有字符,且不忽略空格,则生成段落,否则不生成
if (($trim_front = trim($front)) !== '' || ($front !== '' && !$ignore_space)) {
$arr[] = ['type' => 'text', 'data' => ['text' => CQ::decode($trim_text ? $trim_front : $front)]];
}
// 处理 CQ 码
$content = mb_substr($end, 4);
$cq = explode(",", $content);
$object_type = array_shift($cq);
$object_params = [];
foreach ($cq as $v) {
$key = mb_strstr($v, "=", true);
$object_params[$key] = CQ::decode(mb_substr(mb_strstr($v, "="), 1), true);
}
$arr[] = ["type" => $object_type, "data" => $object_params];
$msg = mb_substr(mb_strstr($rear, ']'), 1);
}
if (($trim_msg = trim($msg)) !== '' || ($msg !== '' && !$ignore_space)) {
$arr[] = ['type' => 'text', 'data' => ['text' => CQ::decode($trim_text ? $trim_msg : $msg)]];
}
return $arr;
}
/**
* 数组转字符串
* 纪念一下这段代码完全由AI生成没有人知道它是怎么写的这句话是我自己写的不知道是不是有人知道的
* @param array $array
* @return string
* @author Copilot
*/
public static function arrayToStr(array $array): string {
$str = "";
foreach ($array as $v) {
if ($v['type'] == 'text') {
$str .= $v['data']['text'];
} else {
$str .= "[CQ:" . $v['type'];
foreach ($v['data'] as $key => $value) {
$str .= "," . $key . "=" . CQ::encode($value, true);
}
$str .= "]";
}
}
return $str;
}
} }

View File

@@ -11,6 +11,17 @@ use ZM\Framework;
use ZM\Store\Lock\SpinLock; use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic; use ZM\Store\ZMAtomic;
use ZM\Store\ZMBuf; use ZM\Store\ZMBuf;
use function file_get_contents;
use function get_included_files;
use function is_callable;
use function is_string;
use function json_decode;
use function mb_substr;
use function md5_file;
use function pathinfo;
use function server;
use function str_replace;
use function substr;
class ZMUtil class ZMUtil
{ {
@@ -45,15 +56,15 @@ class ZMUtil
* 在工作进程中返回可以通过reload重新加载的php文件列表 * 在工作进程中返回可以通过reload重新加载的php文件列表
* @return string[]|string[][] * @return string[]|string[][]
*/ */
public static function getReloadableFiles() { public static function getReloadableFiles(): array {
return array_map( $array_map = [];
function ($x) { foreach (array_diff(
return str_replace(DataProvider::getSourceRootDir() . '/', '', $x); get_included_files(),
}, array_diff( Framework::$loaded_files
get_included_files(), ) as $key => $x) {
Framework::$loaded_files $array_map[$key] = str_replace(DataProvider::getSourceRootDir() . '/', '', $x);
) }
); return $array_map;
} }
/** /**
@@ -61,10 +72,10 @@ class ZMUtil
* @param $dir * @param $dir
* @param $base_namespace * @param $base_namespace
* @param null|mixed $rule * @param null|mixed $rule
* @param bool $return_kv * @param bool $return_path_value
* @return String[] * @return String[]
*/ */
public static function getClassesPsr4($dir, $base_namespace, $rule = null, $return_kv = false) { public static function getClassesPsr4($dir, $base_namespace, $rule = null, $return_path_value = false): array {
// 预先读取下composer的file列表 // 预先读取下composer的file列表
$composer = json_decode(file_get_contents(DataProvider::getSourceRootDir() . '/composer.json'), true); $composer = json_decode(file_get_contents(DataProvider::getSourceRootDir() . '/composer.json'), true);
$classes = []; $classes = [];
@@ -72,17 +83,17 @@ class ZMUtil
$files = DataProvider::scanDirFiles($dir, true, true); $files = DataProvider::scanDirFiles($dir, true, true);
foreach ($files as $v) { foreach ($files as $v) {
$pathinfo = pathinfo($v); $pathinfo = pathinfo($v);
if ($pathinfo['extension'] == 'php') { if (($pathinfo['extension'] ?? '') == 'php') {
if ($rule === null) { //规则未设置回调时候,使用默认的识别过滤规则 if ($rule === null) { //规则未设置回调时候,使用默认的识别过滤规则
if (substr(file_get_contents($dir . '/' . $v), 6, 6) == '#plain') continue; if (substr(file_get_contents($dir . '/' . $v), 6, 6) == '#plain') continue;
elseif (mb_substr($v, 0, 7) == 'global_' || mb_substr($v, 0, 7) == 'script_') continue; elseif (mb_substr($pathinfo["basename"], 0, 7) == 'global_' || mb_substr($pathinfo["basename"], 0, 7) == 'script_') continue;
foreach (($composer['autoload']['files'] ?? []) as $fi) { foreach (($composer['autoload']['files'] ?? []) as $fi) {
if (md5_file(DataProvider::getSourceRootDir().'/'.$fi) == md5_file($dir.'/'.$v)) continue 2; if (md5_file(DataProvider::getSourceRootDir().'/'.$fi) == md5_file($dir.'/'.$v)) continue 2;
} }
} elseif (is_callable($rule) && !($rule($dir, $pathinfo))) continue; } elseif (is_callable($rule) && !($rule($dir, $pathinfo))) continue;
$dirname = $pathinfo['dirname'] == '.' ? '' : (str_replace('/', '\\', $pathinfo['dirname']) . '\\'); $dirname = $pathinfo['dirname'] == '.' ? '' : (str_replace('/', '\\', $pathinfo['dirname']) . '\\');
$class_name = $base_namespace . '\\' . $dirname . $pathinfo['filename']; $class_name = $base_namespace . '\\' . $dirname . $pathinfo['filename'];
if ($return_kv) $classes[$class_name] = $v; if (is_string($return_path_value)) $classes[$class_name] = $return_path_value . "/" .$v;
else $classes[] = $class_name; else $classes[] = $class_name;
} }
} }

59
src/ZM/ZMServer.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
namespace ZM;
use ZM\Command\RunServerCommand;
use ZM\Console\Console;
use ZM\Event\EventManager;
use ZM\Exception\InitException;
use ZM\Module\ModuleBase;
/**
* @since 2.6
*/
class ZMServer
{
protected $app_name;
/** @var ModuleBase[] */
protected $modules = [];
public function __construct($app_name) {
$this->app_name = $app_name;
}
public function addModule($module_class) {
$this->modules[] = $module_class;
}
public function run() {
Console::setLevel(4);
foreach ($this->modules as $module_class) {
foreach ($module_class->getEvents() as $event) {
EventManager::addEvent(get_class($event), $event);
}
}
echo "Running...\n";
if (defined("WORKDING_DIR")) throw new InitException();
_zm_env_check();
define("WORKING_DIR", getcwd());
define("SOURCE_ROOT_DIR", WORKING_DIR);
define("LOAD_MODE", is_dir(SOURCE_ROOT_DIR . "/src/ZM") ? 0 : 1);
define("FRAMEWORK_ROOT_DIR", realpath(__DIR__ . "/../../"));
define("ZM_VERSION_ID", ConsoleApplication::VERSION_ID);
define("ZM_VERSION", ConsoleApplication::VERSION);
$options = array_map(function ($x) {
return $x->getDefault();
}, RunServerCommand::exportDefinition()->getOptions());
(new Framework($options, true))->start();
}
/**
* @return mixed
*/
public function getAppName() {
return $this->app_name;
}
}

View File

@@ -29,6 +29,19 @@ function getClassPath($class_name) {
if (file_exists($dir2)) return $dir2; if (file_exists($dir2)) return $dir2;
else return null; else return null;
} }
/**
* 检查炸毛框架运行的环境
* @internal
*/
function _zm_env_check() {
if (!extension_loaded("swoole")) die(zm_internal_errcode("E00001") . "Can not find swoole extension.\n");
if (version_compare(SWOOLE_VERSION, "4.5.0") == -1) die(zm_internal_errcode("E00002") . "You must install swoole version >= 4.5.0 !");
if (version_compare(PHP_VERSION, "7.2") == -1) die(zm_internal_errcode("E00003") . "PHP >= 7.2 required.");
if (version_compare(SWOOLE_VERSION, "4.6.7") < 0 && !extension_loaded("pcntl")) {
Console::error(zm_internal_errcode("E00004") . "Swoole 版本必须不低于 4.6.7 或 PHP 安装加载了 pcntl 扩展!");
die();
}
}
/** /**
* 使用自己定义的万san能分割函数 * 使用自己定义的万san能分割函数
@@ -331,19 +344,33 @@ function zm_dump($var, ...$moreVars) {
return $var; return $var;
} }
function zm_info($obj) { Console::info($obj); } function zm_info($obj) {
Console::info($obj);
}
function zm_warning($obj) { Console::warning($obj); } function zm_warning($obj) {
Console::warning($obj);
}
function zm_success($obj) { Console::success($obj); } function zm_success($obj) {
Console::success($obj);
}
function zm_debug($obj) { Console::debug($obj); } function zm_debug($obj) {
Console::debug($obj);
}
function zm_verbose($obj) { Console::verbose($obj); } function zm_verbose($obj) {
Console::verbose($obj);
}
function zm_error($obj) { Console::error($obj); } function zm_error($obj) {
Console::error($obj);
}
function zm_config($name, $key = null) { return ZMConfig::get($name, $key); } function zm_config($name, $key = null) {
return ZMConfig::get($name, $key);
}
function quick_reply_closure($reply) { function quick_reply_closure($reply) {
return function () use ($reply) { return function () use ($reply) {

View File

@@ -7,5 +7,4 @@ function loader__generated_id__() {
} }
} }
echo "OK!\n";
return json_decode(file_get_contents(__DIR__.'/zmplugin.json'), true) ?? ['zm_module' => false]; return json_decode(file_get_contents(__DIR__.'/zmplugin.json'), true) ?? ['zm_module' => false];

View File

@@ -0,0 +1,63 @@
<?php
namespace ZM\Utils;
use PHPUnit\Framework\TestCase;
use Swoole\WebSocket\Frame;
use ZM\Requests\ZMRequest;
class MessageUtilTest extends TestCase
{
public function setUp(): void {
ZMRequest::websocket();
$a = new Frame();
$a->opcode = WEBSOCKET_OPCODE_PONG;
}
public function testGetImageCQFromLocal() {
file_put_contents("/tmp/a.jpg", "fake photo");
$this->assertEquals(
MessageUtil::getImageCQFromLocal("/tmp/a.jpg"),
"[CQ:image,file=base64://".base64_encode("fake photo")."]"
);
}
public function testSplitCommand() {
$msg_sample_1 = "你好啊 233\n\nhello";
$msg_sample_2 = "";
$this->assertCount(3, MessageUtil::splitCommand($msg_sample_1));
$this->assertCount(1, MessageUtil::splitCommand($msg_sample_2));
}
public function testIsAtMe() {
$this->assertTrue(MessageUtil::isAtMe("[CQ:at,qq=123]", 123));
$this->assertFalse(MessageUtil::isAtMe("[CQ:at,qq=]", 0));
}
public function testDownloadCQImage() {
if (file_exists(WORKING_DIR."/zm_data/images/abc.jpg"))
unlink(WORKING_DIR."/zm_data/images/abc.jpg");
ob_start();
$msg = "[CQ:image,file=abc.jpg,url=https://zhamao.xin/file/hello.jpg]";
$result = MessageUtil::downloadCQImage($msg, "/home/jerry/fweewfwwef/wef");
$this->assertFalse($result);
$this->assertStringContainsString("E00059", ob_get_clean());
$result = MessageUtil::downloadCQImage($msg);
$this->assertIsArray($result);
$this->assertFileExists(WORKING_DIR."/zm_data/images/abc.jpg");
$result = MessageUtil::downloadCQImage($msg.$msg);
$this->assertIsArray($result);
$this->assertCount(2, $result);
}
public function testContainsImage() {
$msg_sample = "hello\n[CQ:imag2]";
$this->assertFalse(MessageUtil::containsImage($msg_sample));
$this->assertTrue(MessageUtil::containsImage($msg_sample."[CQ:image,file=123]"));
}
public function testMatchCommand() {
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace ZM\Utils;
use PHPUnit\Framework\TestCase;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Store\LightCacheInside;
use ZM\Store\ZMAtomic;
class TerminalTest extends TestCase
{
public function setUp(): void {
}
public function testExecuteCommand() {
ob_start();
Terminal::executeCommand("logtest");
$this->assertStringContainsString("debug msg", ob_get_clean());
}
public function testBc() {
ob_start();
Terminal::executeCommand("bc ".base64_encode("echo 'hello';"));
$this->assertStringContainsString("hello", ob_get_clean());
}
}

View File

@@ -16,4 +16,9 @@ class ZMUtilTest extends TestCase
$this->assertContains(TimerMiddleware::class, ZMUtil::getClassesPsr4(DataProvider::getSourceRootDir()."/src/Module", "Module")); $this->assertContains(TimerMiddleware::class, ZMUtil::getClassesPsr4(DataProvider::getSourceRootDir()."/src/Module", "Module"));
$this->assertContains(Framework::class, ZMUtil::getClassesPsr4(DataProvider::getSourceRootDir()."/src/ZM", "ZM")); $this->assertContains(Framework::class, ZMUtil::getClassesPsr4(DataProvider::getSourceRootDir()."/src/ZM", "ZM"));
} }
public function testGetModInstance() {
$class = Hello::class;
$this->assertTrue(ZMUtil::getModInstance($class) instanceof Hello);
}
} }

Some files were not shown because too many files have changed in this diff Show More