Compare commits

...

91 Commits
2.0.2 ... 2.2.9

Author SHA1 Message Date
jerry
a23f3d8f16 update to 2.2.9 version
update reply() to support quick operation
fix reload bug
fix reply() bug
2021-03-06 17:22:42 +08:00
jerry
0c24bfdedd fix a motd bug 2021-03-02 21:31:06 +08:00
jerry
c0b95c6840 delete workflows 2021-03-02 21:27:04 +08:00
jerry
e977b09e20 Merge remote-tracking branch 'origin/master' 2021-03-02 21:24:53 +08:00
jerry
4ff75cf199 update to 2.2.8 version
update motd message
2021-03-02 21:24:31 +08:00
Whale
24e70c70ce Update deploy-docs.yml 2021-03-02 14:27:55 +08:00
Whale
275a7bf00b Update deploy-docs.yml 2021-03-02 14:26:02 +08:00
Whale
455fc79818 Update deploy-docs.yml 2021-03-02 14:22:40 +08:00
Whale
8740c3c255 Update deploy-docs.yml 2021-03-02 14:19:51 +08:00
Whale
98bfca5bb9 Update deploy-docs.yml 2021-03-02 14:18:40 +08:00
Whale
fc8d01ad5f Update deploy-docs.yml 2021-03-02 13:53:12 +08:00
Whale
d9b8df1725 Update deploy-docs.yml 2021-03-02 13:50:52 +08:00
Whale
9b7802ac04 Update deploy-docs.yml 2021-03-02 13:50:39 +08:00
Whale
6e1f3e0406 Update deploy-docs.yml 2021-03-02 13:43:45 +08:00
Whale
a2d4bab062 Update index.md 2021-03-02 13:40:21 +08:00
Whale
f1cefad910 Create deploy-docs.yml 2021-03-02 13:37:07 +08:00
jerry
957c69bd1e update to 2.2.7 version
fix reply() bug
fix access_token bug
2021-02-27 16:19:18 +08:00
Whale
2902c5e805 Merge pull request #33 from YiwanGi/master
Update ServerEventHandler.php
2021-02-27 16:13:46 +08:00
Wang
faf9f5d988 Update ServerEventHandler.php
-When the token is incorrect, repeated connection events occur in the framework
2021-02-27 00:04:02 +08:00
Whale
874f061468 Update README.md 2021-02-26 11:05:04 +08:00
jerry
69521a1f1f cleanup code, update some features
add Hitokoto API
add Closure for access_token
add working_dir() global function
adjust reply() method to .handle_quick_operation
2021-02-24 23:37:00 +08:00
Whale
fb9dbed306 Merge pull request #32 from wen1014/master
warning bug fix
2021-02-23 23:24:45 +08:00
Whale
d42158ac90 Merge branch 'master' into master 2021-02-23 23:24:24 +08:00
Whale
ff3ebec562 Merge pull request #31 from YiwanGi/patch-8
Update spin-lock.md
2021-02-23 23:22:01 +08:00
wenhao
ea79de617e warning bug fix 2021-02-23 17:04:10 +08:00
Wang
9e1ad6a983 Update spin-lock.md
-Forgotten data content
2021-02-22 19:34:06 +08:00
Whale
c17248df31 Merge pull request #30 from YiwanGi/patch-7
Update light-cache.md
2021-02-22 11:33:52 +08:00
Whale
4c116ebd86 Merge pull request #29 from YiwanGi/patch-5
Update console.md
2021-02-22 11:33:29 +08:00
Whale
c490fe0c1c Merge pull request #28 from YiwanGi/patch-6
Update route-annotations.md
2021-02-22 11:32:46 +08:00
Whale
cefdf23799 Merge pull request #27 from YiwanGi/patch-4
Update README.md
2021-02-22 11:32:06 +08:00
Wang
7f70642606 Update light-cache.md
-Follow up the latest configuration data
2021-02-22 02:51:35 +08:00
Wang
1d5b2609f9 Update console.md 2021-02-22 02:03:22 +08:00
Wang
a206fe8b87 Update route-annotations.md
-Correction of typos
2021-02-22 01:18:12 +08:00
Wang
fb4f6c45ce Update README.md
-Detail optimization
2021-02-22 01:07:35 +08:00
jerry
c50ae245bd commitment, nothing 2021-02-21 22:17:34 +08:00
Whale
f6c2131ebf Merge pull request #26 from YiwanGi/patch-3
Update README.md
2021-02-21 22:15:39 +08:00
Whale
543d1d2922 Merge pull request #25 from YiwanGi/patch-2
Update basic-config.md
2021-02-21 22:14:27 +08:00
Whale
bb61e6f6a2 Merge pull request #24 from YiwanGi/patch-1
Update quickstart-robot.md
2021-02-21 22:13:06 +08:00
YiwanGi
2d1bbf6b48 Update README.md
-Adjust the display format appropriately
-Solve the problem of no access to images in China
2021-02-21 12:48:54 +08:00
YiwanGi
67e42cfe3e Update basic-config.md
-Better display
2021-02-21 11:28:01 +08:00
YiwanGi
429a2cf230 Update quickstart-robot.md
-Better display
2021-02-21 10:48:23 +08:00
jerry
9ace85e604 update to 2.2.5 version again
add transaction for SpinLock.php
add getAllCQ() for CQ.php
fix CQ bug
update docs
2021-02-20 16:57:19 +08:00
jerry
f677b0e132 update to 2.2.5 version
add saveToJson and loadFromJson function for DataProvider.php
fix @OnSave annotation not working
adjust swoole timer tick
add hasKey() for WorkerCache.php
2021-02-15 15:15:26 +08:00
jerry
f137f044d0 Merge remote-tracking branch 'origin/master' 2021-02-09 17:09:26 +08:00
jerry
77c12db31a reformat code 2021-02-09 17:09:09 +08:00
Whale
b670cb29fe Update README.md 2021-02-09 11:12:29 +08:00
Whale
95d7bb071d Update README.md 2021-02-09 10:54:59 +08:00
Whale
eadb4c1dee Update README.md 2021-02-09 10:54:05 +08:00
Whale
6672a6c852 Update README.md 2021-02-09 10:53:52 +08:00
Whale
094feddda4 Update README.md 2021-02-09 10:53:15 +08:00
Whale
f86eddb298 Update README.md 2021-02-09 10:48:05 +08:00
Whale
a93b4917cd Update README.md 2021-02-09 10:47:40 +08:00
jerry
0f9767aa16 update docs 2021-02-07 11:48:55 +08:00
jerry
0c9f246690 update to 2.2.4 version
update docs
fix broken ssh caused cpu overloading
fix WorkerCache bug when no global config
add global function zm_atomic
2021-02-07 11:46:42 +08:00
jerry
517d258d61 update to 2.2.3 version, I am tired
fix access_token not working
fix waitMessage() not working in v2.2.2
2021-01-30 00:06:42 +08:00
jerry
61e3818563 update to 2.2.2 version finally
clean redundant code
fix API reply in @OnTick for multi-process
fix loop error reporting
2021-01-29 23:34:34 +08:00
jerry
776ec98a3e fix waitMessage timeout bug 2021-01-29 22:32:29 +08:00
jerry
f3e844bb0a update to 2.2.2 version
fix QQBot error
clean code
2021-01-29 22:27:10 +08:00
jerry
a55cd4ed05 update docs 2021-01-29 21:37:02 +08:00
jerry
8a985620f9 update to 2.2.1 version
fix a compatibility bug
2021-01-29 21:36:14 +08:00
jerry
484ddf9dfa update Hello.php
update docs
2021-01-29 21:30:19 +08:00
jerry
b611b4aad6 add DaemonCommand for daemon players
adjust http_header available
2021-01-29 20:47:00 +08:00
jerry
b9f973c718 add unset for WorkerCache.php 2021-01-20 18:43:22 +08:00
jerry
cd6c971547 update Singleton 2021-01-20 16:45:50 +08:00
jerry
c68083484a update to 2.2.0 version
add OnPipeMessageEvent.php
add ProcessManager.php
add WorkerCache component
fix route bug
correct Exception to ZMException
2021-01-20 16:11:04 +08:00
jerry
f999e689bf update docs 2021-01-18 18:09:33 +08:00
jerry
187a08a621 update to 2.1.6 version
优化代码结构
增加更多提示语
修复处理空格消息时的报错
修复上下文的bug
2021-01-18 18:08:29 +08:00
Whale
c208298937 Update README.md 2021-01-14 18:32:54 +08:00
crazywhalecc
e9e3e5e129 update docs 2021-01-13 15:46:55 +08:00
crazywhalecc
1ef8225d10 update to 2.1.5 version
change route to Symfony routing
2021-01-13 15:40:27 +08:00
crazywhalecc
ccadec23e4 update docs and change console command suitable 2021-01-07 16:01:01 +08:00
jerry
0972a1959e update README.md 2021-01-05 23:35:49 +08:00
crazywhalecc
ce74191947 update docs 2021-01-05 16:19:35 +08:00
crazywhalecc
4feeb9519c update docs 2021-01-04 16:59:19 +08:00
crazywhalecc
efee146215 update docs 2021-01-04 16:45:06 +08:00
jerry
96ce7b30d0 add InterruptException catcher to onRequest 2021-01-04 01:35:54 +08:00
jerry
076339baec move warning to framework 2021-01-03 18:16:35 +08:00
jerry
50026be73d fix a bug 2021-01-03 18:13:44 +08:00
jerry
a1ad634926 fix a bug 2021-01-02 20:00:33 +08:00
jerry
0a5defaf29 fix a bug 2021-01-02 19:44:19 +08:00
jerry
557efc47a8 update to 2.1.4 version 2021-01-02 19:40:13 +08:00
jerry
c566f940e0 add custom app version 2021-01-02 18:10:20 +08:00
jerry
381062c6c5 update to 2.1.3 version
fix an AnnotationParser bug which is stupid
2021-01-02 16:36:25 +08:00
jerry
b31876025e update composer.json 2021-01-02 13:35:45 +08:00
jerry
ae8b0acdaa update to 2.1.2 version 2021-01-02 13:35:24 +08:00
jerry
20ca3e7416 update docs 2021-01-02 13:28:26 +08:00
jerry
a1bfc031b8 update to 2.1.1 version 2021-01-02 13:20:16 +08:00
jerry
8a6f8f54a5 update composer.json path 2021-01-02 13:19:39 +08:00
jerry
19f0bffcd8 update composer.json 2021-01-02 13:16:22 +08:00
jerry
6337b626d6 update to 2.1.0 version
add @OnCloseEvent, @OnOpenEvent, @OnMessageEvent, @OnRequestEvent
update EventDispatcher.php and change it to status-based schema
fix @CQBefore bugs
2021-01-02 13:15:50 +08:00
crazywhalecc
7434bac94e update to 2.0.3 version 2020-12-31 16:26:51 +08:00
118 changed files with 4944 additions and 921 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,26 +1,24 @@
<div align="center">
<img src="/resources/images/logo_trans.png" height = "150" alt="炸毛框架"><br>
<img src="https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/logo_trans.png" width = "150" height = "150" alt="炸毛框架"><br>
<h2>炸毛框架</h2>
炸毛框架 (zhamao-framework) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)]()
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes)
[![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
[![Latest Stable Version](http://img.shields.io/packagist/v/zhamao/framework.svg)](https://packagist.org/packages/zhamao/framework)
[![Banner](https://img.shields.io/badge/CQHTTP-v11-black)]()
[![Banner](https://img.shields.io/badge/OneBot-v11-success)](https://github.com/howmanybots/onebot)
[![stupid counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/stupid.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid)
[![TODO counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
[![注解数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/AnnotationBase.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=AnnotationBase)
[![TODO 数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
</div>
## 开发者注意
**开发者 QQ 群670821194**
开发者 QQ 群:**670821194** [点击加入群聊](https://jq.qq.com/?_wv=1027&k=YkNI3AIr)
**当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!**
当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!
**2.0 版本如果有问题请第一时间加群反馈!**
有关 3.0 版本的最新情况,请看这里:[Issue #22](https://github.com/zhamao-robot/zhamao-framework/issues/22)
2.0 版本如果有问题请第一时间加群反馈!
## 简介
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
@@ -46,23 +44,22 @@ public function index() {
框架首先需要部署环境,可以参考下方文档中部署环境和框架的方法进行。
## 文档v2 版本)
查看文档[https://docs-v2.zhamao.xin/](https://docs-v2.zhamao.xin/)
查看文档(国内自建):<https://docs-v2.zhamao.xin/>
备用链接[https://docs-v2.zhamao.me/](https://docs-v2.zhamao.me/)
备用链接(国外托管):<https://docs-v2.zhamao.me/>
自行构建文档:`mkdocs build -d distribute`
## 特点
- 支持多账号
- 原生为多账号设计,支持多个机器人负载均衡
- 使用 Swoole 多工作进程机制和协程加持,尽可能简单的情况下提升了性能
- 灵活的注解事件绑定机制
- 支持下断点调试Psysh
- 易用的上下文,模块内随处可用
- 采用模块化编写,可单独拆装功能
- 常驻内存,全局缓存变量随处使用
- 自带 MySQL、Refis 等数据库连接池等数据库连接方案
- 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口
- 静态文件服务器
- 采用模块化编写,可自由搭配其他 composer 组件
- 常驻内存,全局缓存变量随处使用,提供多种缓存方案
- 自带 MySQL、Redis 等数据库连接池等数据库连接方案
- 本身为 HTTP 服务器、WebSocket 服务器,可以构建属于自己的 HTTP API 接口
- 静态文件服务器,可将前端合并到一起
## 从 v1 升级
炸毛框架 v2 相对 v1 版本改动了不少内容,其中包括框架底层机制、注解事件分发、调试、命名空间等变化,详情可查看上方文档。
@@ -70,21 +67,25 @@ public function index() {
如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。
## 贡献和捐赠
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。
目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
本项目为作者闲暇时间开发,如果觉得好用,不妨进行捐助~你的捐助会让我更加有动力完善插件,感谢你的支持!
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。[捐赠列表](https://github.com/zhamao-robot/thanks)
### 支付宝
![支付宝二维码](/resources/images/alipay_img.jpg)
![支付宝二维码](https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/alipay_img.jpg)
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ我们会作为您捐助了本项目
## 关于
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ627577391或提交 Issue 进行疑难解答
作者的炸毛机器人已从2018年初起稳定运行了**三年**,并且持续迭代
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ[627577391](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes))或提交 Issue 进行疑难解答。
本项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的模块代码做好备份。
@@ -92,4 +93,6 @@ public function index() {
**注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。
在贡献代码时,请保管好自己的全局配置文件中的敏感信息,请勿将带有个人信息的配置文件上传 GitHub 等网站。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)

View File

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

View File

@@ -1,18 +1,18 @@
{
"name": "zhamao/framework",
"description": "High performance QQ robot and web server development framework",
"description": "High performance chat robot and web server development framework",
"minimum-stability": "stable",
"license": "Apache-2.0",
"version": "2.0.2",
"extra": [],
"version": "2.2.9",
"extra": {
"exclude_annotate": [
"src/ZM"
]
},
"authors": [
{
"name": "whale",
"email": "crazysnowcc@gmail.com"
},
{
"name": "swift",
"email": "hugo_swift@yahoo.com"
"name": "jerry",
"email": "admin@zhamao.me"
}
],
"prefer-stable": true,
@@ -22,18 +22,18 @@
],
"require": {
"php": ">=7.2",
"doctrine/annotations": "~1.10",
"ext-json": "*",
"ext-posix": "*",
"doctrine/annotations": "~1.10",
"psy/psysh": "@stable",
"symfony/polyfill-ctype": "^1.20",
"symfony/polyfill-mbstring": "^1.20",
"symfony/console": "^5.1",
"symfony/routing": "^5.1",
"zhamao/connection-manager": "*@dev",
"zhamao/console": "^1.0",
"zhamao/config": "^1.0",
"zhamao/request": "*@dev",
"symfony/routing": "^5.1",
"symfony/polyfill-php80": "^1.20"
"zhamao/request": "*@dev"
},
"suggest": {
"ext-ctype": "*",

View File

@@ -27,7 +27,7 @@ $config['crash_dir'] = $config['zm_data'] . 'crash/';
/** 对应swoole的server->set参数 */
$config['swoole'] = [
'log_file' => $config['crash_dir'] . 'swoole_error.log',
'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算则可把这里改为1使用全局变量
//'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算则可把这里改为1使用全局变量
'dispatch_mode' => 2, //包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode
'max_coroutine' => 300000,
//'task_worker_num' => 4,
@@ -36,13 +36,19 @@ $config['swoole'] = [
/** 轻量字符串缓存,默认开启 */
$config['light_cache'] = [
'size' => 1024, //最多允许储存的条数需要2的倍数
'max_strlen' => 16384, //单行字符串最大长度需要2的倍数
'size' => 512, //最多允许储存的条数需要2的倍数
'max_strlen' => 32768, //单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'].'_cache.json',
'auto_save_interval' => 900
];
/** 大容量跨进程变量存储2.2.0可用) */
$config["worker_cache"] = [
"worker" => 0,
"transaction_timeout" => 30000
];
/** MySQL数据库连接信息host留空则启动时不创建sql连接池 */
$config['sql_config'] = [
'sql_host' => '',
@@ -72,7 +78,7 @@ $config["access_token"] = '';
/** HTTP服务器固定请求头的返回 */
$config['http_header'] = [
'X-Powered-By' => 'zhamao-framework',
'Server' => 'zhamao-framework',
'Content-Type' => 'text/html; charset=utf-8'
];

View File

@@ -1,6 +1,6 @@
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/

View File

@@ -1 +1,3 @@
# FAQ
这里会写一些常见的疑难解答。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
## Swoole\Http\Request
此类是 Swoole 内部的一个类,一般在收到 HTTP 请求时,在 `@RequestMapping``@OnSwooleEvent("request")` 两个注解下可用,用作获取 GET、POST参数上传到后端的文件、Cookies 等。详见 [Swoole 文档 - Request](http://wiki.swoole.com/#/http_server?id=httprequest) 。
此类是 Swoole 内部的一个类,一般在收到 HTTP 请求时,在 `@RequestMapping``@OnRequestEvent()` 两个注解下可用,用作获取 GET、POST参数上传到后端的文件、Cookies 等。详见 [Swoole 文档 - Request](http://wiki.swoole.com/#/http_server?id=httprequest) 。
### 属性

View File

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

224
docs/assets/face_id.html Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,59 @@
# Token 验证
为了保障安全,框架支持给接入的 WebSocket 连接验证 Token如果不设置 Token 同时又将框架的端口暴露在公网将会非常危险。
炸毛框架兼容 OneBot 标准的机器人客户端,所以自带一个 Token 验证器。
关于 Access Token 方面的标准规范,请参考下面内容:
- [OneBot - 鉴权](https://github.com/howmanybots/onebot/blob/master/v11/specs/communication/authorization.md)
- [go-cqhttp - 配置](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md)
> 以 go-cqhttp 举例,如果要设置验证,则将 go-cqhttp 配置文件中的 `access_token` 项填入内容即可。
## 验证位置
框架对 Token 的验证是内置的,在事件 `open`WebSocket 连接接入时)触发。
如果是兼容 OneBot 标准的客户端接入,则一切都是兼容的。
如果是自定义的其他 WebSocket 客户端也想接入框架,那么其他 WebSocket 客户端也需要进行相应的设置才能利用此 Token 验证。
如果验证成功Token 符合要求)则分发事件 `@OnOpenEvent`,否则此事件不触发,同时断开 WebSocket 连接。
## 标准验证(字符串形式)
默认的情况下,在框架的全局配置文件 `global.php` 中,对配置项 `access_token` 填入与 OneBot 客户端相同的 `access_token` 即可实现鉴权。下面是一个最基本的和 go-cqhttp 设置鉴权配置:
go-cqhttp 的配置段:
```hjson
// 访问密钥, 强烈推荐在公网的服务器设置
access_token: "emhhbWFvLXJvYm90"
```
框架的配置文件配置段:
```php
/** onebot连接约定的token */
$config["access_token"] = 'emhhbWFvLXJvYm90';
```
然后重启框架和 go-cqhttp 即可。(其他 OneBot 客户端同理)
## 自定义验证Token 验证)
有些情况下,使用一个单一的字符串可能无法满足你对 Token 验证的安全需求,需要自定义一些判断模式才能满足,所以框架的 `access_token` 配置项支持动态的闭包函数自行编写判断逻辑,例如下面的一个例子,我可以让框架同时允许接入多个不同 token 的 WebSocket 连接:
```php
/** onebot连接约定的token */
$config["access_token"] = function($token){
$allow = ['emhhbWFvLXJvYm90','aXMtdmVyeS1nb29k'];
if (in_array($token, $allow)) return true;
else return false;
};
```
## 自定义验证open 事件)
当然,这里设置了自定义方式,其实你也可以在下一层的 `@OnOpenEvent` 注解事件中进行自定义内容和判断,具体见 `@OnOpenEvent` 的相关章节。

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

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

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

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

View File

@@ -26,19 +26,19 @@ public function hello() {
获取 Swoole WebSocker Server 对象。此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server)。
可以使用的事件:`@OnSwooleEvent("message")``@OnSwooleEvent("open")``@OnSwooleEvent("close")``@OnStart()` 以及所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
可以使用的事件:`@OnMessageEvent()``@OnOpenEvent()``@OnCloseEvent()``@OnStart()` 以及所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
## getFrame() - 获取 WS 数据帧
获取 `\Swoole\Websocket\Frame` 对象,此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)。
可以使用的事件:`@OnSwooleEvent("message")` 以及所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等,
可以使用的事件:`@OnMessageEvent()` 以及所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等,
## getFd() - 返回 fd 值
获取当前连入 Swoole 服务器的连接文件描述符 ID。返回 int。一般代表连接号可用来绑定对应链接。
可以使用的事件:所有 **getFrame()** 可以使用的,`@OnSwooleEvent("open")``@OnSwooleEvent("close")`
可以使用的事件:所有 **getFrame()** 可以使用的,`@OnOpenEvent()``@OnCloseEvent()`
!!! tip "提示"
@@ -51,7 +51,7 @@ public function hello() {
* @CQCommand("测试fd")
*/
public function testfd() {
ctx()->reply("当前机器人连接的fd是".ctx()->getFd()"机器人QQ是".ctx()->getRobotId());
ctx()->reply("当前机器人连接的fd是".ctx()->getFd()."机器人QQ是".ctx()->getRobotId());
}
```
@@ -92,13 +92,13 @@ public function onMessage() {
返回 `\Swoole\Http\Request` 对象,可在 `@RequestMapping` 中使用,获取 Cookie请求头GET 参数什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httprequest)。
可以使用的事件:`@RequestMapping()``@OnSwooleEvent("request")``@OnSwooleEvent("open")`
可以使用的事件:`@RequestMapping()``@OnRequestEvent()``@OnOpenEvent()`
## getResponse() - HTTP 响应对象
返回 `\Swoole\Http\Response` 对象的增强版,可在 HTTP 请求相关的事件中使用,返回内容和设置 Cookie 什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httpresponse)。
可以使用的事件:`@RequestMapping()``@OnSwooleEvent("request")`
可以使用的事件:`@RequestMapping()``@OnRequestEvent()`
下面是使用以上两个功能的组合示例:
@@ -114,7 +114,7 @@ public function ping() {
## getConnection() - WS 连接对象
返回此上下文相关联的 WebSocket 连接对象。
返回此上下文相关联的 WebSocket 连接对象。详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client)。
可以使用的事件:所有 **getFrame()** 可以使用的都可以使用。
@@ -124,7 +124,7 @@ public function ping() {
## getRobot() - 获取机器人 API 对象
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](机器人API.md)。
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](robot-api.md)。
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
@@ -254,7 +254,7 @@ if($r["retcode"] == 0) Console::success("消息发送成功!");
参数同 `reply()`
## waitMessage()
## waitMessage() - 等待用户消息
- 参数:`waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "")`
- 用途:等待用户输入消息
@@ -286,14 +286,140 @@ function yourName(){
( 你都10分钟不理我了嘤嘤嘤
</chat-box>
## getArgs()
## getArgs() - 自动获取参数
TODO还没写到这里下次更新今晚太困了。
`waitMessage()` 的封装,目的是让机器人的回复更加智能化。最好的例子就是在框架自带的默认示例中“随机数”的例子,我们假设要写一个随机数功能,但是用户从来都是不思考就使用机器人的。抛开人工智能,我们能做的就是“专家系统”,同时让我们写的代码尽可能适配用户所说的每一句话:
- 随机数 1 100
- 随机数(一般不知道怎么用这个功能的人都会只说一个关键词)
- 从2到9的随机数
所以,在匹配第一和第二种情况时候,我们不需要重复写代码,而第一种的话用户已经将参数给你的时候,你不需要再次使用 `waitMessage()` 方式进行等待询问,只需要取到使用就好了。`getArgs()` 就是做这个的。
定义:`getArgs($mode, $prompt_msg)`
`$mode`:获取模式,有三种:
- `ZM_MATCH_ALL`:效果等同于 `getFullArg()`,获取全部的内容,把空格也当作一部分
- `ZM_MATCH_NUMBER`:效果等同于 `getNumArg()`,获取下一个数字参数
- `ZM_MATCH_FIRST`:效果等同于 `getNextArg()`,获取下一个参数
`$prompt_msg`:字符串,指定如果参数缺失时询问用户的内容。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getArgs(ZM_MATCH_FIRST, "请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<chat-box>
) test
( 请输入你要传入的参数内容
) test2
( 参数内容test2
</chat-box>
`getArgs()` 也有三层封装,在使用过程中避免麻烦的话,推荐使用下面这几种 `get*Arg()` 方式。
## getFullArg()
获取关键词后的整个字符串参数,包括空格,如果不存在则询问。
典型例子:`复读机 你好 你好`,获取参数时会将 `你好 你好` 当作一个参数来获取。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getFullArg("请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<chat-box>
) test abc def argtest
( 参数内容abc def argtest
) test
( 请输入你要传入的参数内容
) abc def
( 参数内容abc def
</chat-box>
## getNextArg()
获取下一个参数分隔符可以是空格tab。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getNextArg("请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<chat-box>
) test abc def argtest
( 参数内容abc
) test
( 请输入你要传入的参数内容
) abc
( 参数内容abc
</chat-box>
## getNumArg()
> 2.1.5 版本起可用。
获取下一个数字型参数,如果 `is_numeric()` 为 true 则获取成功,如果没有符合的则询问用户。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getNextArg("请输入你要传入的数字内容");
return "数字参数内容:".$s;
}
```
<chat-box>
) test abc 334 argtest
( 数字参数内容334
) test abc
( 请输入你要传入的数字内容
) 998
( 参数内容998
</chat-box>
## copy()
t
获取整个上下文的所有内容的数组形式。
```php
$arr = ctx()->copy();
dump($arr);
```
## getOption() - 获取匹配参数内容
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
return "参数内容:".implode(", ", ctx()->getOption());
}
```
<chat-box>
) test abc 334 argtest
( 参数内容abc, 334, argtest
</chat-box>

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
# 存储管理(文件)
DataProvider 是框架内提供的一个简易的文件管理类。
定义:`\ZM\Utils\DataProvider`
## DataProvider::getWorkingDir()
`working_dir()`
## DataProvider::getFrameworkLink()
`ZMConfig::get("global", "http_reverse_link")`,获取反向代理的链接。
## DataProvider::getDataFolder()
获取配置项 `zm_data` 指定的目录。
## DataProvider::saveToJson()
将变量内容保存为 json 格式的文件,存储在 `zm_data/config/` 目录下或子目录下。
定义:`saveToJson($filename, $file_array)`
`$filename` 是文件名,不需要加后缀,比如你想保存成 `foo/bar.json`,这里写 `foo/bar` 就好。如果不想要二级目录,就直接写 `bar`,不需要加 `.json` 后缀。
这里只支持二级目录,不支持更多级的子目录。
`$file_array` 为内容,一般是数组,比如你缓存了一个 API 接口返回的数据,然后直接解析成数组后丢给它就好了。
## DataProvider::loadFromJson()
从 json 文件加载内容至变量。
定义:`loadFromJson($filename)`
文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。
## 其他文件读取
框架比较贴近原生的 PHP所以推荐直接使用原生的方法来读写文件`file_get_contents``file_put_contents`)。但有一点要注意,框架内最好使用**工作目录或者绝对路径**。
```php
// 读取框架工作目录的文件 composer.json 文件
$r = file_get_contents(working_dir() . "/composer.json");
// 写入 Linux 临时目录下的文件
file_put_contents("/tmp/test.txt", "hello world");
```
!!! warning "注意"
在默认的情况里,框架的根目录均为可写可读的,在读写文件时务必要注意目录的位置和权限。使用 `working_dir()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

View File

@@ -4,7 +4,7 @@
!!! quote "开发提示"
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不嗦。
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不嗦。
## 控制器和路由
@@ -228,4 +228,4 @@ public function staticImage($param) {
}
```
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。

View File

@@ -1,3 +0,0 @@
# 中间件注解
TODO师傅莫催快肝完了

View File

@@ -1,72 +0,0 @@
# 框架核心注解事件
框架核心注解事件区别于机器人和路由注解事件,这里框架注解事件都是**直接**或封装调用 Swoole 的回调事件的,所以对一些比较底层或者基础的操作都在这里做,例如收到 HTTP 或 WebSocket 连接后执行的事件函数。
## OnSwooleEvent()
绑定 Swoole 所相关的事件,例如 WebSocket 接入、收到 WS 消息、关闭 WS 连接HTTP 请求到达等。
### 属性
| 类型 | 值 |
| ------------ | ------------------------------------------ |
| 名称 | `@OnSwooleEvent` |
| 触发前提 | 当参数指定的 `type` 对应的事件被触发后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnSwooleEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
| 注解绑定参数 | |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | -------------------------------------------------------- | ----------------------------------------------- | ---------------- |
| type | `string`,支持填入 `open``request``close``message` | 限定事件的类型,**必填** | |
| rule | `string`,必须是可执行且返回 bool 的 PHP 代码 | 例如判断连接是否为 QQ 机器人(`connectIsQQ()` | 空rule 为 true |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/inside-class/) 类型,返回一个当前 WS 连接的连接对象。
### 示例1机器人连接框架后输出信息
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
class Hello {
/**
* 在机器人客户端连接框架后向终端输出信息
* @OnSwooleEvent("open",rule="connectIsQQ()")
* @param $conn
*/
public function onConnect(ConnectionObject $conn) {
Console::info("机器人 " . $conn->getOption("connect_id") . " 已连接!");
}
}
```
这里的 Console 是终端输出组件,详情见组件一栏对应的文档查询。
### 示例2阻断 Chrome 访问框架时多访问一次的问题)
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Event\EventDispatcher;
class Hello {
/**
* 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰
* @OnSwooleEvent("request",rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
*/
public function onRequest() {
EventDispatcher::interrupt();
}
}
```
其中 EventDispatcher 为事件分发器interrupt 是通用阻断方法,如果你平常只使用阻断,则只需掌握这一个方法即可,`EventDispatcher::interrupt()` 在所有事件内可用。

View File

@@ -4,34 +4,35 @@
!!! error "警告"
因为炸毛框架的全局配置中含有数据库名称和密码以及 access_token 等敏感字段,在使用版本控制软件过程中请不要将敏感信息写入配置文件并提交至开源仓库!
因为炸毛框架的全局配置中含有数据库名称和密码以及 access_token 等敏感字段,在使用版本控制软件过程中请不要将敏感信息写入配置文件并提交至开源仓库!
## 全局配置文件 global.php
框架的全局配置文件在 `config/global.php` 文件中。下面是配置文件的各个选项,请根据自己的需要自行配置。
| 配置名称 | 说明 | 默认值 |
| :--------------------------- | ------------------------------------------------ | ---------------------------- |
| `host` | 框架监听的地址 | 0.0.0.0 |
| `port` | 框架监听的端口 | 20001 |
| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 |
| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` |
| `debug_mode` | 框架是否启动 debug 模式 | false |
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
| `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` |
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` |
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
| `access_token` | OneBot 客户端连接约定的token留空则无 | 空 |
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 |
| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 |
| `info_level` | 终端日志显示等级0-4 | 2 |
| `context_class` | 上下文所定义的类,待上下文完善后见对应文档 | `\ZM\Context\Context::class` |
| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` |
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表 | 见配置文件 |
| `command_register_class` | 注册自定义命令行选项指令的类 | 见配置文件 |
| `modules` | 服务器启用的外部第三方和内部插件 | `['onebot' => true]` |
| 配置名称 | 说明 | 默认值 |
| :--------------------------- | ------------------------------------------------------------ | ---------------------------- |
| `host` | 框架监听的地址 | 0.0.0.0 |
| `port` | 框架监听的端口 | 20001 |
| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 |
| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` |
| `debug_mode` | 框架是否启动 debug 模式 | false |
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
| `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` |
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` |
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
| `access_token` | OneBot 客户端连接约定的token留空则无相关设置见 [组件 - Access Token 验证](component/access-token) | 空 |
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 |
| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 |
| `info_level` | 终端日志显示等级0-4 | 2 |
| `context_class` | 上下文所定义的类,待上下文完善后见对应文档 | `\ZM\Context\Context::class` |
| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` |
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表 | 见配置文件 |
| `command_register_class` | 注册自定义命令行选项指令的类 | 见配置文件 |
| `modules` | 服务器启用的外部第三方和内部插件 | `['onebot' => true]` |
### 子表 **swoole**
@@ -46,12 +47,18 @@
| 配置名称 | 说明 | 默认值 |
| -------------------------- | ----------------------------------------------- | ---------------------------- |
| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 1024 |
| `max_strlen` | 作为 value 字符串的最大长度 | 16384 |
| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 512 |
| `max_strlen` | 作为 value 字符串的最大长度 | 32768 |
| `hash_conflict_proportion` | Hash冲突率越大越好但是需要的内存更多 | 0.6 |
| `persistence_path` | 持久化的键值对的存储路径 | `zm_data` 下的 `_cache.json` |
| `auto_save_interval` | 持久化的键值对自动保存时间间隔(秒) | 900 |
### 子表 worker_cache
| 配置名称 | 说明 | 默认值 |
| -------- | --------------------------- | ------ |
| `worker` | 跨进程缓存的存储工作进程 id | 0 |
### 子表 **sql_config**
| 配置名称 | 说明 | 默认值 |
@@ -84,11 +91,11 @@
## 多环境下的配置文件
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境(production / development / staging来编写配置文件。
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境development / staging / production)来编写配置文件。
### 使用环境参数
在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `production``staging``development`
在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `development``staging``production`
```bash
vendor/bin/start server --env=development
@@ -96,7 +103,7 @@ vendor/bin/start server --env=development
### 不同环境配置文件
由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制或改名为 `global.development.php``global.production.php` 即可。
由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制并重命名为 `global.development.php``global.production.php` 即可。
### 优先级
@@ -139,4 +146,4 @@ $r = ZMConfig::get("example_a", "key1"); # $r == "value1"
$time = ZMConfig::get("example_a", "starttime"); # $time == 服务器启动时间
```
同时,自定义配置文件也支持环境变量,例如:`example_a.development.json``example_a.production.php` 均可。
同时,自定义配置文件也支持环境变量,例如:`example_a.development.json``example_a.production.php` 均可。

View File

@@ -61,12 +61,12 @@ pecl install swoole
如果你是通过**主机安装 PHP 部署的环境**,下方是通过脚手架来构建项目的命令行。
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter/
composer update
composer create-project zhamao/framework-starter zhamao-app
cd zhamao-app/ # 这个是你可以自己定义的名称
vendor/bin/start server # 启动框架
```
如果是通过 **Docker 部署的环境**,则需要在先克隆脚手架后在文件夹内使用 Docker 命令下的 `composer update`
如果是通过 **Docker 部署的环境**,则需要在先克隆脚手架后在文件夹内使用 Docker 命令下的 `composer update`(如果主机环境有 composer 也可以使用 `composer create-project` 的方式拉取脚手架。)
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git
@@ -82,8 +82,19 @@ cd zhamao-framework-starter
./run-docker.sh # 在正式版炸毛框架 v2 发布后可用,测试版暂不放出
```
!!! tip "提示"
如果国内 Composer 下载过慢,可以使用阿里云的 Composer 镜像加速。
```bash
# 仅对当前的项目使用阿里云加速
composer config repo.packagist composer https://mirrors.aliyun.com/composer/
# 对全局的 Composer 使用阿里云加速
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
```
## 启动框架
本地环境启动方式:
```bash
cd zhamao-framework-starter

View File

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

View File

@@ -8,7 +8,7 @@
一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 **机器人问答模块** 的准备。
炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架
炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架。
## 机器人客户端
@@ -30,51 +30,6 @@ OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)
由于 go-cqhttp 项目还处于开发期,而且配置文件格式也发生了多次变化,但大体内容没有变(比如编写此文档时发布的版本中配置文件格式变成了 `hjson` 取代了原来的 `json`
=== "config.json旧格式"
``` json hl_lines="2 3 30 31"
{
"uin": 你的QQ号,
"password": "你的密码",
"encrypt_password": false,
"password_encrypted": "",
"enable_db": true,
"access_token": "",
"relogin": {
"enabled": true,
"relogin_delay": 3,
"max_relogin_times": 0
},
"ignore_invalid_cqcode": false,
"force_fragmented": true,
"heartbeat_interval": 0,
"http_config": {
"enabled": false,
"host": "0.0.0.0",
"port": 5700,
"timeout": 0,
"post_urls": {}
},
"ws_config": {
"enabled": false,
"host": "0.0.0.0",
"port": 6700
},
"ws_reverse_servers": [
{
"enabled": true,
"reverse_url": "ws://127.0.0.1:20001/",
"reverse_api_url": "",
"reverse_event_url": "",
"reverse_reconnect_interval": 3000
}
],
"post_message_format": "string",
"debug": false,
"log_level": ""
}
```
=== "config.hjson新格式"
``` json hl_lines="3 5 81 84"
@@ -193,6 +148,51 @@ OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)
}
```
=== "config.json旧格式"
``` json hl_lines="2 3 30 31"
{
"uin": 你的QQ号,
"password": "你的密码",
"encrypt_password": false,
"password_encrypted": "",
"enable_db": true,
"access_token": "",
"relogin": {
"enabled": true,
"relogin_delay": 3,
"max_relogin_times": 0
},
"ignore_invalid_cqcode": false,
"force_fragmented": true,
"heartbeat_interval": 0,
"http_config": {
"enabled": false,
"host": "0.0.0.0",
"port": 5700,
"timeout": 0,
"post_urls": {}
},
"ws_config": {
"enabled": false,
"host": "0.0.0.0",
"port": 6700
},
"ws_reverse_servers": [
{
"enabled": true,
"reverse_url": "ws://127.0.0.1:20001/",
"reverse_api_url": "",
"reverse_event_url": "",
"reverse_reconnect_interval": 3000
}
],
"post_message_format": "string",
"debug": false,
"log_level": ""
}
```
其中 ws://127.0.0.1:20001/ 中的 127.0.0.1 和 20001 应分别对应炸毛框架配置的 HOST 和 PORT
## 第一次对话
@@ -224,5 +224,14 @@ public function repeat() {
这样,一个简易的复读机就做好了!回到 QQ 机器人聊天,向机器人发送 `echo 你好啊`,它会回复你 `你好啊`
<chat-box>
) echo 你好啊
( 你好啊
) echo
( 请输入你要回复的内容
) 哦豁
( 哦豁
</chat-box>
> 如果你只回复 `echo` 的话,它会先和你进入一个会话状态,并问你 `请输入你要回复的内容`,这时你再次说一些内容例如 `哦豁`,会回复你 `哦豁`。效果和直接输入 `echo 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,163 @@
# 更新日志v2 版本)
## v2.2.9
> 更新时间2021.3.6
- 更新:`reply()` 方法传入数组则变为快速相应的 API 操作
- 修复:在 Worker 进程下调用 `ZMUtil::reload()` 会导致一些奇怪的 bug
- 修复:`reply()` 时会 at 私聊成员的 bug由 go-cqhttp 导致)
## v2.2.8
> 更新时间2021.3.2
- 更新MOTD 显示的方式,更加直观和炫酷
## v2.2.7
> 更新时间2021.2.27
- 修复2.2.6 版本下 `reply()` 方法在群里调用会 at 成员的 bug
- 修复:空 `access_token` 的情况下会无法连入的 bug
- 修复:使用 Closure 闭包函数自行编写逻辑的判断返回 false 无法阻断连接的 bug
## v2.2.6
> 更新时间2021.2.26
- 新增:`uuidgen()` 全局函数,快速生成 uuid
- 修复MySQL `rawQuery()` 在参数为非数组时会报 Warning 的 bug
- 新增:示例模块的 API 示例:一言查询
- 优化:删减部分无用代码
- 更改:`ctx()->reply()` 方法改为调用隐藏方法:`.handle_quick_operation`
- 修复:`ctx()->finalReply()` 一直以来的 bug未阻断事件
- 新增:`access_token` 配置项支持闭包函数自行设计判断方式和逻辑
- 新增:全局函数 `working_dir()`
## v2.2.5
> 更新时间2021.2.20
- 新增:`saveToJson()``loadFromJson()` 方法DataProvider 类)
- 修复:`@OnSave` 注解事件无法工作的 bug
- 调整:自定义计时器创建时的性能调优
- 新增WorkerCache 方法:`hasKey()`
- 新增SpinLock 方法:`transaction()`(直接在事务中上锁)
- 新增CQ 方法:`getAllCQ()``_custom()`(获取消息中的所有 CQ 码)
- 修复CQ 类中的部分 bug
## v2.2.4
> 更新时间2021.2.7
- 修复:终端交互导致的 ssh 断掉后 CPU 占用过高的问题
- 修复WorkerCache 在缺少配置文件下工作异常的问题
- 新增:全局函数:`zm_atomic()`
## v2.2.3
> 更新时间2021.1.30
- 修复waitMessage() 在 v2.2.2 版本中不可用的 bug
- 修复access_token 无效的问题
## v2.2.2
> 更新时间2021.1.29
- 修复:模块文件错误时避免循环报错
- 优化:代码结构
- 修复:在不同进程时调用机器人 API 无法返回且报错的 bug
- **修复机器人无法连接的问题2.1.6 ~ 2.2.1 受影响)**
## v2.2.1
> 更新时间2021.1.29
- 修复:配置文件兼容性问题
## v2.2.0
> 更新时间2021.1.29
- 新增:`@OnPipeMessageEvent` 注解
- 新增:进程管理器
- 新增:`--daemon` 守护进程化后查看状态以及一系列操作的命令行
- 新增WorkerCache
- 修复:路由问题
- 修复:`http_header` 配置项不生效的 bug
- 优化:框架内部所有异常全部基于 `ZMException`
- 优化SingletonTrait 支持扩展
## v2.1.6
> 更新时间2021.1.18
- 优化:代码结构
- 增加:更多提示语
- 修复:处理空格消息时的报错
- 修复上下文的bug
## v2.1.5
> 更新时间2021.1.13
- 优化:终端对 PHP Warning 和 PHP Notice 的报错信息显示,统一格式
- 新增:`ctx()->getNumArg()` 上下文中快速获取数字类型的参数的方法
- 优化:删除不必要的调试信息
- 优化:路由组件全面替换为 `symfony/routing`,兼容性和稳定性 up
## v2.1.4
> 更新时间2021.1.3
- 修复:启动时会提示丢失类的 bug
- 优化HTTP 响应类如果被使用了则一律返回 false
- 优化PHP Warning 等报错统一样式
## v2.1.3
> 更新时间2021.1.2
- 修复:注解解析器在某种特殊情况下导致的 bug
## v2.1.2
> 更新时间2021.1.2
- 修复:引入包模式启动时会导致的满屏报错
## v2.1.1
> 更新时间2021.1.2
- 修复:自定义加载注解选定 composer.json 文件错误的 bug
## v2.1.0
> 更新时间2021.1.2
- 新增:`@OnOpenEvent``@OnCloseEvent``@OnMessageEvent``@OnRequestEvent`
- 优化事件分发器,修复一些事件分发过程中的 bug
- 修复 `@CQBefore` 事件的 bug
## v2.0.3
> 更新时间2020.12.31
- 修复CQBefore 注解事件在 level 低于 200 时无法调用的 bug
- 修复CQMetaEvent 注解事件调用时报错的 bug
## v2.0.2
> 更新时间2020.12.31
- 更新:将 CQ 码调用类更新到与最新 OneBot 标准相兼容的状态
## v2.0.1
> 更新事件2020.12.23
> 更新时间2020.12.23
- 修复:开屏报错文件夹不存在

View File

@@ -9,6 +9,9 @@ theme:
logo: assets/logos.png
favicon: assets/favicon.png
language: zh
palette:
primary: indigo
accent: indigo
features:
- navigation.tabs
extra_javascript:
@@ -31,7 +34,7 @@ extra:
version:
method: mike
copyright: 'Copyright &copy; 2019 - 2020 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
copyright: 'Copyright &copy; 2019 - 2021 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
<button data-md-color-scheme="default"><code>默认模式</code></button>
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
</span>
@@ -52,30 +55,50 @@ copyright: 'Copyright &copy; 2019 - 2020 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&n
nav:
- 指南:
- 介绍: index.md
- 安装框架: guide/安装.md
- 快速上手(机器人篇): guide/快速上手-机器人.md
- 快速上手HTTP篇: guide/快速上手-http.md
- 选择聊天机器人实例: guide/OneBot实例.md
- 基本配置: guide/基本配置.md
- 编写模块: guide/编写模块.md
- 注册事件响应: guide/注册事件响应.md
- 安装框架: guide/installation.md
- 快速上手(机器人篇): guide/quickstart-robot.md
- 快速上手HTTP篇: guide/quickstart-http.md
- 选择聊天机器人实例: guide/onebot-choose.md
- 基本配置: guide/basic-config.md
- 编写模块: guide/write-module.md
- 注册事件响应: guide/register-event.md
- 事件和注解:
- 事件和注解: event/index.md
- 机器人注解事件: event/机器人注解事件.md
- HTTP 路由注解事件: event/路由注解事件.md
- 框架核心注解事件: event/框架注解事件.md
- 中间件注解: event/中间件.md
- 自定义注解: event/自定义注解.md
- 事件分发器: event/事件分发器.md
- 机器人注解事件: event/robot-annotations.md
- HTTP 路由注解事件: event/route-annotations.md
- 框架核心注解事件: event/framework-annotations.md
- 中间件注解: event/middleware.md
- 自定义注解: event/custom-annotations.md
- 事件分发器: event/event-dispatcher.md
- 框架组件:
- 框架组件: component/index.md
- 机器人 API: component/机器人API.md
- CQ 码(多媒体消息): component/CQ码.md
- 上下文: component/上下文.md
- 机器人 API: component/robot-api.md
- CQ 码(多媒体消息): component/cqcode.md
- 上下文: component/context.md
- 存储:
- LightCache 轻量缓存: component/light-cache.md
- MySQL 数据库: component/mysql.md
- Redis 数据库: component/redis.md
- ZMAtomic 原子计数器: component/atomics.md
- SpinLock 自旋锁: component/spin-lock.md
- 文件管理: component/data-provider.md
- 协程池: component/coroutine-pool.md
- 单例类: component/singleton-trait.md
- ZMUtil 杂项: component/zmutil.md
- 全局方法: component/global-functions.md
- HTTP 和 WebSocket 客户端: component/zmrequest.md
- Console 终端: component/console.md
- Token 验证: component/access-token.md
- 进阶开发:
- 进阶开发: advanced/index.md
- 框架剖析: advanced/framework-structure.md
- 框架启动模式: advanced/custom-start.md
- 从 v1 升级: advanced/to-v2.md
- 内部类文件手册: advanced/inside-class.md
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
- 框架多进程: advanced/multi-process.md
- 开发实战教程:
- 编写管理员才能触发的功能: advanced/example/admin.md
- FAQ: FAQ.md
- 更新日志:
- 更新日志v2: update/v2.md

View File

@@ -1,31 +0,0 @@
<?php
namespace Custom\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CustomCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'custom';
protected function configure() {
$this->setDescription("custom description | 自定义命令的描述字段");
$this->addOption("failure", null, null, "以错误码为1返回结果");
// ...
}
protected function execute(InputInterface $input, OutputInterface $output) {
if ($input->getOption("failure")) {
$output->writeln("<error>Hello error! I am wrong message.</error>");
return Command::FAILURE;
} else {
$output->writeln("<comment>Hello world! I am successful message.</comment>");
return Command::SUCCESS;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,17 +3,16 @@
namespace ZM\API;
use Co;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
use ZM\Utils\CoMessage;
trait CQAPI
{
/**
* @param ConnectionObject $connection
* @param $connection
* @param $reply
* @param |null $function
* @return bool|array
@@ -35,21 +34,20 @@ trait CQAPI
$r[$api_id] = [
"data" => $reply,
"time" => microtime(true),
"self_id" => $connection->getOption("connect_id")
"self_id" => $connection->getOption("connect_id"),
"echo" => $api_id
];
if ($function === true) $r[$api_id]["coroutine"] = Co::getuid();
LightCacheInside::set("wait_api", "wait_api", $r);
SpinLock::unlock("wait_api");
if (server()->push($connection->getFd(), json_encode($reply))) {
if ($function === true) {
Co::suspend();
return CoMessage::yieldByWS($r[$api_id], ["echo"], 60);
} else {
SpinLock::lock("wait_api");
$r = LightCacheInside::get("wait_api", "wait_api");
$data = $r[$api_id];
unset($r[$api_id]);
LightCacheInside::set("wait_api", "wait_api", $r);
SpinLock::unlock("wait_api");
return isset($data['result']) ? $data['result'] : null;
}
return true;
} else {

View File

@@ -59,7 +59,7 @@ class ZMRobot
public static function getAllRobot() {
$r = ManagerGM::getAllByName('qq');
$obj = [];
foreach($r as $v) {
foreach ($r as $v) {
$obj[] = new ZMRobot($v);
}
return $obj;
@@ -228,9 +228,9 @@ class ZMRobot
/**
* 群组单人禁言
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
* @param int $group_id
* @param int $user_id
* @param int $duration
* @param $group_id
* @param $user_id
* @param $duration
* @return array|bool|null
*/
public function setGroupBan($group_id, $user_id, $duration = 1800) {

View File

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

View File

@@ -3,7 +3,6 @@
namespace ZM\Annotation\CQ;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,9 @@ namespace ZM;
use Exception;
use ZM\Command\DaemonReloadCommand;
use ZM\Command\DaemonStatusCommand;
use ZM\Command\DaemonStopCommand;
use ZM\Command\InitCommand;
use ZM\Command\PureHttpCommand;
use ZM\Command\RunServerCommand;
@@ -23,24 +26,21 @@ class ConsoleApplication extends Application
public function initEnv() {
$this->selfCheck();
if (!is_dir(__DIR__ . '/../../vendor')) {
define("LOAD_MODE", 1); // composer项目模式
define("LOAD_MODE_COMPOSER_PATH", getcwd());
} else {
define("LOAD_MODE", 0); // 源码模式
}
//if (LOAD_MODE === 0) $this->add(new BuildCommand()); //只有在git源码模式才能使用打包指令
if (LOAD_MODE === 0) define("WORKING_DIR", getcwd());
elseif (LOAD_MODE == 1) define("WORKING_DIR", realpath(__DIR__ . "/../../"));
elseif (LOAD_MODE == 2) echo "Phar mode: " . WORKING_DIR . PHP_EOL;
if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) {
/** @noinspection PhpIncludeInspection */
require_once DataProvider::getWorkingDir() . "/vendor/autoload.php";
}
if (LOAD_MODE == 2) {
// Phar 模式2.0 不提供哦
//require_once FRAMEWORK_DIR . "/vendor/autoload.php";
spl_autoload_register('phar_classloader');
} elseif (LOAD_MODE == 0) {
/** @noinspection PhpIncludeInspection
* @noinspection RedundantSuppression
*/
require_once WORKING_DIR . "/vendor/autoload.php";
echo "* This is repository mode.\n";
if (LOAD_MODE == 0) {
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
if (!isset($composer["autoload"]["psr-4"]["Module\\"])) {
echo "框架源码模式需要在autoload文件中添加Module目录为自动加载是否添加[Y/n] ";
@@ -64,6 +64,9 @@ class ConsoleApplication extends Application
}
$this->addCommands([
new DaemonStatusCommand(),
new DaemonReloadCommand(),
new DaemonStopCommand(),
new RunServerCommand(), //运行主服务的指令控制器
new InitCommand(), //初始化用的用于项目初始化和phar初始化
new PureHttpCommand() //纯HTTP服务器指令
@@ -75,6 +78,7 @@ class ConsoleApplication extends Application
if (!($obj instanceof Command)) throw new TypeError("Command register class must be extended by Symfony\\Component\\Console\\Command\\Command");
$this->add($obj);
}*/
return $this;
}
/**
@@ -91,14 +95,9 @@ class ConsoleApplication extends Application
}
private function selfCheck() {
if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19");
if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19\n");
if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !");
//if (!extension_loaded("gd")) die("Can not find gd extension.\n");
//if (!extension_loaded("sockets")) die("Can not find sockets extension.\n");
if (substr(PHP_VERSION, 0, 1) < "7") die("PHP >=7 required.\n");
//if (!function_exists("curl_exec")) die("Can not find curl extension.\n");
//if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n");
//if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n");
if (version_compare(PHP_VERSION, "7.2") == -1) die("PHP >= 7.2 required.");
return true;
}
}

View File

@@ -12,6 +12,7 @@ use swoole_server;
use ZM\ConnectionManager\ConnectionObject;
use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console;
use ZM\Event\EventDispatcher;
use ZM\Exception\InvalidArgumentException;
use ZM\Exception\WaitTimeoutException;
use ZM\Http\Response;
@@ -105,28 +106,35 @@ class Context implements ContextInterface
* @return mixed
*/
public function reply($msg, $yield = false) {
switch ($this->getData()["message_type"]) {
case "group":
case "private":
case "discuss":
$this->setCache("has_reply", true);
$data = $this->getData();
$conn = $this->getConnection();
switch ($data["message_type"]) {
case "group":
return (new ZMRobot($conn))->setCallback($yield)->sendGroupMsg($data["group_id"], $msg);
case "private":
return (new ZMRobot($conn))->setCallback($yield)->sendPrivateMsg($data["user_id"], $msg);
}
return null;
$data = $this->getData();
$conn = $this->getConnection();
if (!is_array($msg)) {
switch ($this->getData()["message_type"]) {
case "group":
case "private":
case "discuss":
$this->setCache("has_reply", true);
$operation["reply"] = $msg;
$operation["at_sender"] = false;
return (new ZMRobot($conn))->setCallback($yield)->callExtendedAPI(".handle_quick_operation", [
"context" => $data,
"operation" => $operation
]);
}
return false;
} else {
$operation = $msg;
return (new ZMRobot($conn))->setCallback(false)->callExtendedAPI(".handle_quick_operation", [
"context" => $data,
"operation" => $operation
]);
}
return false;
}
public function finalReply($msg, $yield = false) {
self::$context[$this->cid]["cache"]["block_continue"] = true;
if ($msg == "") return true;
return $this->reply($msg, $yield);
if ($msg != "") $this->reply($msg, $yield);
EventDispatcher::interrupt();
}
/**
@@ -145,7 +153,7 @@ class Context implements ContextInterface
if ($prompt != "") $this->reply($prompt);
try {
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())]);
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())], $timeout);
} catch (Exception $e) {
$r = false;
}
@@ -242,6 +250,14 @@ class Context implements ContextInterface
*/
public function getFullArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_ALL, $prompt_msg); }
/**
* @param string $prompt_msg
* @return int|mixed|string
* @throws InvalidArgumentException
* @throws WaitTimeoutException
*/
public function getNumArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_NUMBER, $prompt_msg); }
public function cloneFromParent() {
set_coroutine_params(self::$context[Co::getPcid()] ?? self::$context[$this->cid]);
return context();

View File

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

View File

@@ -31,7 +31,6 @@ class DB
/**
* @param $table_name
* @param bool $enable_cache
* @return Table
* @throws DbException
*/
@@ -39,7 +38,7 @@ class DB
if (Table::getTableInstance($table_name) === null) {
if (in_array($table_name, self::$table_list))
return new Table($table_name);
elseif(SqlPoolStorage::$sql_pool !== null){
elseif (SqlPoolStorage::$sql_pool !== null) {
throw new DbException("Table " . $table_name . " not exist in database.");
} else {
throw new DbException("Database connection not exist or connect failed. Please check sql configuration");
@@ -85,7 +84,8 @@ class DB
* @throws DbException
*/
public static function rawQuery(string $line, $params = [], $fetch_mode = ZM_DEFAULT_FETCH_MODE) {
Console::debug("MySQL: ".$line." | ". implode(", ", $params));
if (!is_array($params)) $params = [$params];
Console::debug("MySQL: " . $line . " | " . implode(", ", $params));
try {
$conn = SqlPoolStorage::$sql_pool->get();
if ($conn === false) {
@@ -95,6 +95,7 @@ class DB
$ps = $conn->prepare($line);
if ($ps === false) {
SqlPoolStorage::$sql_pool->put(null);
/** @noinspection PhpUndefinedFieldInspection */
throw new DbException("SQL语句查询错误" . $line . ",错误信息:" . $conn->error);
} else {
if (!($ps instanceof PDOStatement) && !($ps instanceof PDOStatementProxy)) {
@@ -115,7 +116,7 @@ class DB
return $ps->fetchAll($fetch_mode);
}
} catch (DbException $e) {
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
if (mb_strpos($e->getMessage(), "has gone away") !== false) {
zm_sleep(0.2);
Console::warning("Gone away of MySQL! retrying!");
return self::rawQuery($line, $params);
@@ -123,7 +124,7 @@ class DB
Console::warning($e->getMessage());
throw $e;
} catch (PDOException $e) {
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
if (mb_strpos($e->getMessage(), "has gone away") !== false) {
zm_sleep(0.2);
Console::warning("Gone away of MySQL! retrying!");
return self::rawQuery($line, $params);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,11 @@ use Exception;
use Swoole\Timer;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnSave;
use ZM\Annotation\Swoole\OnTick;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Store\LightCache;
use ZM\Store\ZMAtomic;
class EventManager
@@ -22,6 +25,7 @@ class EventManager
public static function addEvent($event_name, ?AnnotationBase $event_obj) {
self::$events[$event_name][] = $event_obj;
(new AnnotationParser())->sortByLevel(self::$events, $event_name);
}
public static function loadEventByParser(AnnotationParser $parser) {
@@ -37,7 +41,7 @@ class EventManager
public static function registerTimerTick() {
$dispatcher = new EventDispatcher(OnTick::class);
foreach (self::$events[OnTick::class] ?? [] as $vss) {
if (server()->worker_id !== $vss->worker_id) return;
if (server()->worker_id !== $vss->worker_id && $vss->worker_id != -1) return;
//echo server()->worker_id.PHP_EOL;
$plain_class = $vss->class;
Console::debug("Added Middleware-based timer: " . $plain_class . " -> " . $vss->method);
@@ -58,5 +62,11 @@ class EventManager
}
});
}
$conf = ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if (server()->worker_id == $conf["worker"]) {
zm_timer_tick(ZMConfig::get("global", "light_cache")["auto_save_interval"] * 1000, function () {
LightCache::savePersistence();
});
}
}
}

View File

@@ -1,21 +1,29 @@
<?php /** @noinspection PhpComposerExtensionStubsInspection */
<?php /** @noinspection PhpUnreachableStatementInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
namespace ZM\Event;
use Closure;
use Co;
use Error;
use Exception;
use PDO;
use ReflectionException;
use Swoole\Coroutine;
use Swoole\Database\PDOConfig;
use Swoole\Database\PDOPool;
use Swoole\Event;
use Swoole\Process;
use Swoole\Timer;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnMessageEvent;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnPipeMessageEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Config\ZMConfig;
@@ -30,12 +38,15 @@ use ZM\Context\Context;
use ZM\Context\ContextInterface;
use ZM\DB\DB;
use ZM\Exception\DbException;
use ZM\Exception\InterruptException;
use ZM\Framework;
use ZM\Http\Response;
use ZM\Module\QQBot;
use ZM\Store\LightCache;
use ZM\Store\LightCacheInside;
use ZM\Store\MySQL\SqlPoolStorage;
use ZM\Store\Redis\ZMRedisPool;
use ZM\Store\WorkerCache;
use ZM\Store\ZMBuf;
use ZM\Utils\DataProvider;
use ZM\Utils\HttpUtil;
@@ -53,7 +64,12 @@ class ServerEventHandler
if ($terminal_id !== null) {
ZMBuf::$terminal = $r = STDIN;
Event::add($r, function () use ($r) {
$var = trim(fgets($r));
$fget = fgets($r);
if ($fget === false) {
Event::del($r);
return;
}
$var = trim($fget);
try {
Terminal::executeCommand($var, $r);
} catch (Exception $e) {
@@ -64,12 +80,24 @@ class ServerEventHandler
});
}
Process::signal(SIGINT, function () use ($r) {
echo "\r";
Console::warning("Server interrupted by keyboard on Master.");
if ((Framework::$server->inotify ?? null) !== null)
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
ZMUtil::stop();
if (zm_atomic("_int_is_reload")->get() === 1) {
zm_atomic("_int_is_reload")->set(0);
ZMUtil::reload();
} else {
echo "\r";
Console::warning("Server interrupted(SIGINT) on Master.");
if ((Framework::$server->inotify ?? null) !== null)
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
ZMUtil::stop();
}
});
if (Framework::$argv["daemon"]) {
$daemon_data = json_encode([
"pid" => \server()->master_pid,
"stdout" => ZMConfig::get("global")["swoole"]["log_file"]
], 128 | 256);
file_put_contents(DataProvider::getWorkingDir() . "/.daemon_pid", $daemon_data);
}
if (Framework::$argv["watch"]) {
if (extension_loaded('inotify')) {
Console::warning("Enabled File watcher, do not use in production.");
@@ -78,11 +106,11 @@ class ServerEventHandler
$this->addWatcher(DataProvider::getWorkingDir() . "/src", $fd);
Event::add($fd, function () use ($fd) {
$r = inotify_read($fd);
var_dump($r);
dump($r);
ZMUtil::reload();
});
} else {
Console::warning("You have not loaded inotify extension.");
Console::warning("You have not loaded \"inotify\" extension, please install first.");
}
}
}
@@ -100,6 +128,9 @@ class ServerEventHandler
* @param $worker_id
*/
public function onWorkerStop(Server $server, $worker_id) {
if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) {
LightCache::savePersistence();
}
Console::debug(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止");
}
@@ -114,7 +145,7 @@ class ServerEventHandler
Console::debug("正在关闭 " . ($server->taskworker ? "Task" : "") . "Worker 进程 " . Console::setColor("#" . \server()->worker_id, "gold") . TermColor::frontColor256(59) . ", pid=" . posix_getpid());
server()->stop($worker_id);
});
unset(Context::$context[Co::getCid()]);
unset(Context::$context[Coroutine::getCid()]);
if ($server->taskworker === false) {
try {
register_shutdown_function(function () use ($server) {
@@ -194,7 +225,7 @@ class ServerEventHandler
// 开箱即用的Redis
$redis = ZMConfig::get("global", "redis_config");
if($redis !== null && $redis["host"] != "") {
if ($redis !== null && $redis["host"] != "") {
if (!extension_loaded("redis")) Console::error("Can not find redis extension.\n");
else ZMRedisPool::init($redis);
}
@@ -211,7 +242,8 @@ class ServerEventHandler
return server()->worker_id === $v->worker_id || $v->worker_id === -1;
});
$dispatcher->dispatchEvents($server, $worker_id);
Console::debug("@OnStart 执行完毕");
if ($dispatcher->status === EventDispatcher::STATUS_NORMAL) Console::debug("@OnStart 执行完毕");
else Console::warning("@OnStart 执行异常!");
} catch (Exception $e) {
Console::error("Worker加载出错停止服务");
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
@@ -221,7 +253,7 @@ class ServerEventHandler
Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
Console::error("Maybe it caused by your own code if in your own Module directory.");
Console::log($e->getTraceAsString(), 'gray');
ZMUtil::stop();
posix_kill($server->master_pid, SIGINT);
}
} else {
// 这里是TaskWorker初始化的内容部分
@@ -238,7 +270,7 @@ class ServerEventHandler
Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
Console::error("Maybe it caused by your own code if in your own Module directory.");
Console::log($e->getTraceAsString(), 'gray');
ZMUtil::stop();
posix_kill($server->master_pid, SIGINT);
}
}
}
@@ -250,10 +282,17 @@ class ServerEventHandler
*/
public function onMessage($server, Frame $frame) {
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd.": ".TermColor::ITALIC.$frame->data.TermColor::RESET);
unset(Context::$context[Co::getCid()]);
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd . ": " . TermColor::ITALIC . $frame->data . TermColor::RESET);
unset(Context::$context[Coroutine::getCid()]);
$conn = ManagerGM::get($frame->fd);
set_coroutine_params(["server" => $server, "frame" => $frame, "connection" => $conn]);
$dispatcher1 = new EventDispatcher(OnMessageEvent::class);
$dispatcher1->setRuleFunction(function ($v) {
/** @noinspection PhpUnreachableStatementInspection */
return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";");
});
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
$dispatcher->setRuleFunction(function ($v) {
if ($v->getRule() == '') {
@@ -268,6 +307,7 @@ class ServerEventHandler
});
try {
//$starttime = microtime(true);
$dispatcher1->dispatchEvents($conn);
$dispatcher->dispatchEvents($conn);
//Console::success("Used ".round((microtime(true) - $starttime) * 1000, 3)." ms!");
} catch (Exception $e) {
@@ -287,12 +327,20 @@ class ServerEventHandler
* @param $request
* @param $response
*/
public function onRequest($request, $response) {
public function onRequest(?Request $request, ?\Swoole\Http\Response $response) {
$response = new Response($response);
foreach (ZMConfig::get("global")["http_header"] as $k => $v) {
$response->setHeader($k, $v);
}
unset(Context::$context[Co::getCid()]);
Console::debug("Calling Swoole \"request\" event from fd=" . $request->fd);
set_coroutine_params(["request" => $request, "response" => $response]);
$dis1 = new EventDispatcher(OnRequestEvent::class);
$dis1->setRuleFunction(function ($v) {
return eval("return " . $v->getRule() . ";") ? true : false;
});
$dis = new EventDispatcher(OnSwooleEvent::class);
$dis->setRuleFunction(function ($v) {
if ($v->getRule() == '') {
@@ -305,8 +353,9 @@ class ServerEventHandler
});
try {
$no_interrupt = $dis->dispatchEvents($request, $response);
if ($no_interrupt !== null) {
$dis1->dispatchEvents($request, $response);
$dis->dispatchEvents($request, $response);
if ($dis->status === EventDispatcher::STATUS_NORMAL && $dis1->status === EventDispatcher::STATUS_NORMAL) {
$result = HttpUtil::parseUri($request, $response, $request->server["request_uri"], $node, $params);
if ($result === true) {
ctx()->setCache("params", $params);
@@ -318,14 +367,16 @@ class ServerEventHandler
$div->request_method = $node["request_method"];
$div->class = $node["class"];
//Console::success("正在执行路由:".$node["method"]);
$r = $dispatcher->dispatchEvent($div, null, $params, $request, $response);
if (is_string($r) && !$response->isEnd()) $response->end($r);
$dispatcher->dispatchEvent($div, null, $params, $request, $response);
if (is_string($dispatcher->store) && !$response->isEnd()) $response->end($dispatcher->store);
}
}
if (!$response->isEnd()) {
//Console::warning('返回了404');
HttpUtil::responseCodePage($response, 404);
}
} catch (InterruptException $e) {
// do nothing
} catch (Exception $e) {
$response->status(500);
Console::info($request->server["remote_addr"] . ":" . $request->server["remote_port"] .
@@ -337,7 +388,7 @@ class ServerEventHandler
else
$response->end("Internal server error.");
}
Console::error("Internal server exception (500), caused by " . get_class($e).": ".$e->getMessage());
Console::error("Internal server exception (500), caused by " . get_class($e) . ": " . $e->getMessage());
Console::log($e->getTraceAsString(), "gray");
} catch (Error $e) {
$response->status(500);
@@ -351,7 +402,7 @@ class ServerEventHandler
else
$response->end("Internal server error.");
}
Console::error("Internal server error (500), caused by " . get_class($e).": ".$e->getMessage());
Console::error("Internal server error (500), caused by " . get_class($e) . ": " . $e->getMessage());
Console::log($e->getTraceAsString(), "gray");
}
}
@@ -364,13 +415,33 @@ class ServerEventHandler
public function onOpen($server, Request $request) {
Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd);
unset(Context::$context[Co::getCid()]);
$type = strtolower($request->get["type"] ?? $request->header["x-client-role"] ?? "");
$type = strtolower($request->header["x-client-role"] ?? $request->get["type"] ?? "");
$access_token = explode(" ", $request->header["authorization"] ?? "")[1] ?? $request->get["token"] ?? "";
$token = ZMConfig::get("global", "access_token");
if ($token instanceof Closure) {
if (!$token($access_token)) {
$server->close($request->fd);
Console::warning("Unauthorized access_token: " . $access_token);
return;
}
} elseif (is_string($token)) {
if ($access_token !== $token && $token !== "") {
$server->close($request->fd);
Console::warning("Unauthorized access_token: " . $access_token);
return;
}
}
$type_conn = ManagerGM::getTypeClassName($type);
ManagerGM::pushConnect($request->fd, $type_conn);
$conn = ManagerGM::get($request->fd);
set_coroutine_params(["server" => $server, "request" => $request, "connection" => $conn, "fd" => $request->fd]);
$conn->setOption("connect_id", strval($request->header["x-self-id"] ?? ""));
$dispatcher1 = new EventDispatcher(OnOpenEvent::class);
$dispatcher1->setRuleFunction(function ($v) {
return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";");
});
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
$dispatcher->setRuleFunction(function ($v) {
if ($v->getRule() == '') {
@@ -387,6 +458,7 @@ class ServerEventHandler
LightCacheInside::set("connect", "conn_fd", $request->fd);
}
}
$dispatcher1->dispatchEvents($conn);
$dispatcher->dispatchEvents($conn);
} catch (Exception $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
@@ -412,6 +484,10 @@ class ServerEventHandler
Console::debug("Calling Swoole \"close\" event from fd=" . $fd);
set_coroutine_params(["server" => $server, "connection" => $conn, "fd" => $fd]);
$dispatcher1 = new EventDispatcher(OnCloseEvent::class);
$dispatcher1->setRuleFunction(function ($v) {
return $v->connect_type == ctx()->getConnection()->getName() && eval("return " . $v->getRule() . ";");
});
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
$dispatcher->setRuleFunction(function ($v) {
@@ -429,6 +505,7 @@ class ServerEventHandler
LightCacheInside::set("connect", "conn_fd", -1);
}
}
$dispatcher1->dispatchEvents($conn);
$dispatcher->dispatchEvents($conn);
} catch (Exception $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
@@ -444,9 +521,10 @@ class ServerEventHandler
/**
* @SwooleHandler("pipeMessage")
* @param $server
* @param Server $server
* @param $src_worker_id
* @param $data
* @throws Exception
*/
public function onPipeMessage(Server $server, $src_worker_id, $data) {
//var_dump($data, $server->worker_id);
@@ -457,28 +535,80 @@ class ServerEventHandler
$obj = $data["data"];
Co::resume($obj["coroutine"]);
break;
case "stop":
Console::verbose('正在清理 #' . $server->worker_id . ' 的计时器');
Timer::clearAll();
case "getWorkerCache":
$r = WorkerCache::get($data["key"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case "terminate":
$server->stop();
case "setWorkerCache":
$r = WorkerCache::set($data["key"], $data["value"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case 'echo':
Console::success('接收到来自 #' . $src_worker_id . ' 的消息');
case "unsetWorkerCache":
$r = WorkerCache::unset($data["key"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case 'send':
$server->sendMessage(json_encode(["action" => "echo"]), $data["target"]);
case "hasKeyWorkerCache":
$r = WorkerCache::hasKey($data["key"], $data["subkey"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case "asyncAddWorkerCache":
WorkerCache::add($data["key"], $data["value"], true);
break;
case "asyncSubWorkerCache":
WorkerCache::sub($data["key"], $data["value"], true);
break;
case "asyncSetWorkerCache":
WorkerCache::set($data["key"], $data["value"], true);
break;
case "asyncUnsetWorkerCache":
WorkerCache::unset($data["key"], true);
break;
case "addWorkerCache":
$r = WorkerCache::add($data["key"], $data["value"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case "subWorkerCache":
$r = WorkerCache::sub($data["key"], $data["value"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
$server->sendMessage(json_encode($action, 256), $src_worker_id);
break;
case "returnWorkerCache":
WorkerCache::$transfer[$data["cid"]] = $data["value"];
zm_resume($data["cid"]);
break;
default:
echo $data . PHP_EOL;
$dispatcher = new EventDispatcher(OnPipeMessageEvent::class);
$dispatcher->setRuleFunction(function (OnPipeMessageEvent $v) use ($data) {
return $v->action == $data["action"];
});
$dispatcher->dispatchEvents($data);
break;
}
}
/**
* @SwooleHandler("task")
* @param Server|null $server
* @param Server\Task $task
* @return mixed
* @noinspection PhpUnusedParameterInspection
*/
public function onTask() {
public function onTask(?Server $server, Server\Task $task) {
$data = $task->data;
switch ($data["action"]) {
case "runMethod":
$c = $data["class"];
$ss = new $c();
$method = $data["method"];
$ps = $data["params"];
$task->finish($ss->$method(...$ps));
}
return null;
}
/**
@@ -505,7 +635,16 @@ class ServerEventHandler
//加载各个模块的注解类,以及反射
Console::debug("检索Module中");
$parser = new AnnotationParser();
$parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/Module/", "Module");
$path = DataProvider::getWorkingDir() . "/src/";
$dir = scandir($path);
unset($dir[0], $dir[1]);
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
foreach ($dir as $v) {
if (is_dir($path . "/" . $v) && isset($composer["autoload"]["psr-4"][$v . "\\"]) && !in_array($composer["autoload"]["psr-4"][$v . "\\"], $composer["extra"]["exclude_annotate"] ?? [])) {
Console::verbose("Add " . $v . " to register path");
$parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/" . $v . "/", $v);
}
}
$parser->registerMods();
EventManager::loadEventByParser($parser); //加载事件
@@ -518,14 +657,14 @@ class ServerEventHandler
//加载插件
$plugins = ZMConfig::get("global", "modules") ?? [];
if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false];
if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false, "message_level" => 99999];
if ($plugins["onebot"]) {
$obj = new OnSwooleEvent();
$obj->class = QQBot::class;
$obj->method = 'handle';
$obj->type = 'message';
$obj->level = 99999;
$obj->level = $plugins["onebot"]["message_level"] ?? 99999;
$obj->rule = 'connectIsQQ()';
EventManager::addEvent(OnSwooleEvent::class, $obj);
if ($plugins["onebot"]["single_bot_mode"]) {
@@ -536,7 +675,6 @@ class ServerEventHandler
}
//TODO: 编写加载外部插件的方式
$this->loadExternalModules($plugins);
}
private function addWatcher($maindir, $fd) {
@@ -550,11 +688,4 @@ class ServerEventHandler
}
}
}
private function loadExternalModules($plugins) {
foreach ($plugins as $k => $v) {
if ($k == "onebot") continue;
}
}
}

View File

@@ -9,6 +9,7 @@ use Exception;
use ZM\Annotation\Swoole\OnSetup;
use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM;
use ZM\Console\TermColor;
use ZM\Event\ServerEventHandler;
use ZM\Store\LightCache;
use ZM\Store\LightCacheInside;
@@ -55,9 +56,9 @@ class Framework
ZMAtomic::init();
try {
$sw = ZMConfig::get("global");
if(!is_dir($sw["zm_data"])) mkdir($sw["zm_data"]);
if(!is_dir($sw["config_dir"])) mkdir($sw["config_dir"]);
if(!is_dir($sw["crash_dir"])) mkdir($sw["crash_dir"]);
if (!is_dir($sw["zm_data"])) mkdir($sw["zm_data"]);
if (!is_dir($sw["config_dir"])) mkdir($sw["config_dir"]);
if (!is_dir($sw["crash_dir"])) mkdir($sw["crash_dir"]);
ManagerGM::init(ZMConfig::get("global", "swoole")["max_connection"] ?? 2048, 0.5, [
[
"key" => "connect_id",
@@ -73,8 +74,7 @@ class Framework
die($e->getMessage());
}
try {
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
$this->server_set = ZMConfig::get("global", "swoole");
Console::init(
ZMConfig::get("global", "info_level") ?? 2,
self::$server,
@@ -85,36 +85,41 @@ class Framework
$timezone = ZMConfig::get("global", "timezone") ?? "Asia/Shanghai";
date_default_timezone_set($timezone);
$this->server_set = ZMConfig::get("global", "swoole");
$this->parseCliArgs(self::$argv);
$out = [
"host" => ZMConfig::get("global", "host"),
"port" => ZMConfig::get("global", "port"),
"log_level" => Console::getLevel(),
"version" => ZM_VERSION,
"config" => $args["env"] === null ? 'global.php' : $args["env"]
];
// 打印初始信息
$out["listen"] = ZMConfig::get("global", "host") . ":" . ZMConfig::get("global", "port");
if (!isset(ZMConfig::get("global", "swoole")["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)";
else $out["worker"] = ZMConfig::get("global", "swoole")["worker_num"];
$out["environment"] = $args["env"] === null ? "default" : $args["env"];
$out["log_level"] = Console::getLevel();
$out["version"] = ZM_VERSION;
if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION;
if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) {
$out["task_worker_num"] = ZMConfig::get("global", "swoole")["task_worker_num"];
$out["task_worker"] = ZMConfig::get("global", "swoole")["task_worker_num"];
}
if (($num = ZMConfig::get("global", "swoole")["worker_num"] ?? swoole_cpu_num()) != 1) {
$out["worker_num"] = $num;
if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") {
$conf = ZMConfig::get("global", "sql_config");
$out["mysql_pool"] = $conf["sql_database"] . "@" . $conf["sql_host"] . ":" . $conf["sql_port"];
}
if (ZMConfig::get("global", "redis_config")["host"] !== "") {
$conf = ZMConfig::get("global", "redis_config");
$out["redis_pool"] = $conf["host"] . ":" . $conf["port"];
}
if (ZMConfig::get("global", "static_file_server")["status"] !== false) {
$out["static_file_server"] = "enabled";
}
$out["working_dir"] = DataProvider::getWorkingDir();
Console::printProps($out, $tty_width);
self::printProps($out, $tty_width, $args["log-theme"] === null);
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
self::$server->set($this->server_set);
if (file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) {
$motd = file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt");
} else {
$motd = file_get_contents(__DIR__ . "/../../config/motd.txt");
}
$motd = explode("\n", $motd);
foreach ($motd as $k => $v) {
$motd[$k] = substr($v, 0, $tty_width);
}
$motd = implode("\n", $motd);
echo $motd;
self::printMotd($tty_width);
global $asd;
$asd = get_included_files();
// 注册 Swoole Server 的事件
@@ -129,13 +134,62 @@ class Framework
LightCache::init($r);
LightCacheInside::init();
SpinLock::init($r["size"]);
set_error_handler(function ($error_no, $error_msg, $error_file, $error_line) {
switch ($error_no) {
case E_WARNING:
$level_tips = 'PHP Warning: ';
break;
case E_NOTICE:
$level_tips = 'PHP Notice: ';
break;
case E_DEPRECATED:
$level_tips = 'PHP Deprecated: ';
break;
case E_USER_ERROR:
$level_tips = 'User Error: ';
break;
case E_USER_WARNING:
$level_tips = 'User Warning: ';
break;
case E_USER_NOTICE:
$level_tips = 'User Notice: ';
break;
case E_USER_DEPRECATED:
$level_tips = 'User Deprecated: ';
break;
case E_STRICT:
$level_tips = 'PHP Strict: ';
break;
default:
$level_tips = 'Unkonw Type Error: ';
break;
} // do some handle
$error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line;
Console::warning($error); // 如果 return false 则错误会继续递交给 PHP 标准错误处理 /
return true;
}, E_ALL | E_STRICT);
} catch (Exception $e) {
Console::error("Framework初始化出现错误请检查");
Console::error($e->getMessage());
Console::debug($e);
die;
}
}
private static function printMotd($tty_width) {
if (file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) {
$motd = file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt");
} else {
$motd = file_get_contents(__DIR__ . "/../../config/motd.txt");
}
$motd = explode("\n", $motd);
foreach ($motd as $k => $v) {
$motd[$k] = substr($v, 0, $tty_width);
}
$motd = implode("\n", $motd);
echo $motd;
}
public function start() {
self::$server->start();
}
@@ -174,11 +228,9 @@ class Framework
}
}
foreach ($event_list as $k => $v) {
self::$server->on($k, function (...$param) use ($v) {
$c = ZMUtil::getModInstance($v->class);
$m = $v->method;
$c->$m(...$param);
});
$c = ZMUtil::getModInstance($v->class);
$m = $v->method;
self::$server->on($k, function (...$param) use ($c, $m) { $c->$m(...$param); });
}
}
@@ -190,16 +242,7 @@ class Framework
private function parseCliArgs($args) {
$coroutine_mode = true;
global $terminal_id;
$terminal_id = call_user_func(function () {
try {
$data = random_bytes(16);
} catch (Exception $e) {
return "";
}
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)));
});
$terminal_id = uuidgen();
foreach ($args as $x => $y) {
switch ($x) {
case 'disable-coroutine':
@@ -217,11 +260,13 @@ class Framework
case 'daemon':
if ($y) {
$this->server_set["daemonize"] = 1;
Console::$theme = "no-color";
Console::log("已启用守护进程,输出重定向到 " . $this->server_set["log_file"]);
$terminal_id = null;
}
break;
case 'disable-console-input':
case 'no-interaction':
if ($y) $terminal_id = null;
break;
case 'log-error':
@@ -234,6 +279,7 @@ class Framework
if ($y) Console::setLevel(2);
break;
case 'log-verbose':
case 'verbose':
if ($y) Console::setLevel(3);
break;
case 'log-debug':
@@ -244,11 +290,101 @@ class Framework
Console::$theme = $y;
}
break;
default:
//Console::info("Calculating ".$x);
//dump($y);
break;
}
}
if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
}
private static function writeNoDouble($k, $v, &$line_data, &$line_width, &$current_line, $colorful, $max_border) {
$tmp_line = $k . ": " . $v;
//Console::info("写入[".$tmp_line."]");
if (strlen($tmp_line) >= $line_width[$current_line]) { //输出的内容太多了,以至于一行都放不下一个,要折行
$title_strlen = strlen($k . ": ");
$content_len = $line_width[$current_line] - $title_strlen;
$line_data[$current_line] = " " . $k . ": ";
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);
$line_data[$current_line] .= substr($v, 0, $content_len);
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
$rest = substr($v, $content_len);
++$current_line; // 带标题的第一行满了,折到第二行
do {
if ($colorful) $line_data[$current_line] = TermColor::color8(32);
$line_data[$current_line] .= " " . substr($rest, 0, $max_border - 2);
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
$rest = substr($rest, $max_border - 2);
++$current_line;
} while ($rest > $max_border - 2); // 循环,直到放完
} else { // 不需要折行
//Console::info("不需要折行");
$line_data[$current_line] = " " . $k . ": ";
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);
$line_data[$current_line] .= $v;
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
if ($max_border >= 57) {
if (strlen($tmp_line) >= intval(($max_border - 2) / 2)) { // 不需要折行,直接输出一个转下一行
//Console::info("不需要折行,直接输出一个转下一行");
++$current_line;
} else { // 输出很小,写到前面并分片
//Console::info("输出很小,写到前面并分片");
$space = intval($max_border / 2) - 2 - strlen($tmp_line);
$line_data[$current_line] .= str_pad("", $space, " ");
$line_data[$current_line] .= "| "; // 添加分片
$line_width[$current_line] -= (strlen($tmp_line) + 3 + $space);
}
} else {
++$current_line;
}
}
}
public static function printProps($out, $tty_width, $colorful = true) {
$max_border = $tty_width < 65 ? $tty_width : 65;
if (LOAD_MODE == 0) echo Console::setColor("* Framework started with source mode.\n", $colorful ? "yellow" : "");
echo str_pad("", $max_border, "=") . PHP_EOL;
$current_line = 0;
$line_width = [];
$line_data = [];
foreach ($out as $k => $v) {
start:
if (!isset($line_width[$current_line])) {
$line_width[$current_line] = $max_border - 2;
}
//Console::info("行宽[$current_line]".$line_width[$current_line]);
if ($max_border >= 57) { // 很宽的时候,一行能放两个短行
if ($line_width[$current_line] == ($max_border - 2)) { //空行
self::writeNoDouble($k, $v, $line_data, $line_width, $current_line, $colorful, $max_border);
} else { // 不是空行,已经有东西了
$tmp_line = $k . ": " . $v;
//Console::info("[$current_line]即将插入后面的东西[".$tmp_line."]");
if (strlen($tmp_line) > $line_width[$current_line]) { // 地方不够,另起一行
$line_data[$current_line] = str_replace("| ", "", $line_data[$current_line]);
++$current_line;
goto start;
} else { // 地方够,直接写到后面并另起一行
$line_data[$current_line] .= $k . ": ";
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);
$line_data[$current_line] .= $v;
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
++$current_line;
}
}
} else { // 不够宽,直接写单行
self::writeNoDouble($k, $v, $line_data, $line_width, $current_line, $colorful, $max_border);
}
}
foreach ($line_data as $v) {
echo $v . PHP_EOL;
}
echo str_pad("", $max_border, "=") . PHP_EOL;
}
public static function getTtyWidth() {
return explode(" ", trim(exec("stty size")))[1];
}

View File

@@ -4,8 +4,6 @@
namespace ZM\Http;
use ZM\Console\Console;
class Response
{
@@ -94,7 +92,8 @@ class Response
*/
public function status($http_code, $reason = null) {
$this->status_code = $http_code;
return $this->response->status($http_code, $reason);
if (!$this->is_end) return $this->response->status($http_code, $reason);
else return false;
}
public function getStatusCode() {
@@ -107,7 +106,8 @@ class Response
* @return mixed
*/
public function setStatusCode($http_code, $reason = null) {
return $this->response->setStatusCode($http_code, $reason);
if (!$this->is_end) return $this->response->setStatusCode($http_code, $reason);
else return false;
}
/**
@@ -117,7 +117,8 @@ class Response
* @return mixed
*/
public function header($key, $value, $ucwords = null) {
return $this->response->header($key, $value, $ucwords);
if (!$this->is_end) return $this->response->header($key, $value, $ucwords);
else return false;
}
/**
@@ -127,7 +128,7 @@ class Response
* @return mixed
*/
public function setHeader($key, $value, $ucwords = null) {
return $this->response->setHeader($key, $value, $ucwords);
return !$this->is_end ? $this->response->setHeader($key, $value, $ucwords) : false;
}
/**
@@ -159,13 +160,17 @@ class Response
* @return mixed
*/
public function end($content = null) {
$this->is_end = true;
return $this->response->end($content);
if (!$this->is_end) {
$this->is_end = true;
return $this->response->end($content);
} else {
return false;
}
}
public function isEnd() { return $this->is_end; }
public function endWithStatus($status_code = 200, $content = null){
public function endWithStatus($status_code = 200, $content = null) {
$this->status($status_code);
$this->end($content);
}

View File

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

View File

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

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