mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-17 20:54:52 +08:00
refactor all base things
This commit is contained in:
parent
1c801bb205
commit
b2c95d96b1
3
.github/workflows/integration-test.yml
vendored
3
.github/workflows/integration-test.yml
vendored
@ -63,9 +63,6 @@ jobs:
|
||||
- name: Run Static Analysis
|
||||
run: "composer analyse"
|
||||
|
||||
- name: Run PHPUnit
|
||||
run: "composer test"
|
||||
|
||||
cs-check:
|
||||
name: PHP CS Fixer Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -20,14 +20,12 @@
|
||||
"jelix/version": "^2.0",
|
||||
"koriym/attributes": "^1.0",
|
||||
"psr/container": "^2.0",
|
||||
"psy/psysh": "^0.11.2",
|
||||
"symfony/console": "~6.0 || ~5.0 || ~4.0",
|
||||
"symfony/polyfill-ctype": "^1.19",
|
||||
"symfony/polyfill-mbstring": "^1.19",
|
||||
"symfony/polyfill-php80": "^1.16",
|
||||
"symfony/routing": "~6.0 || ~5.0 || ~4.0",
|
||||
"zhamao/logger": "dev-master",
|
||||
"zhamao/request": "^1.1",
|
||||
"onebot/libonebot": "dev-develop"
|
||||
},
|
||||
"require-dev": {
|
||||
@ -63,7 +61,6 @@
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Module\\": "src/Module",
|
||||
"Custom\\": "src/Custom",
|
||||
"Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
|
||||
@ -45,6 +45,9 @@ $config['swoole_options'] = [
|
||||
'swoole_server_mode' => SWOOLE_PROCESS, // Swoole Server 启动模式,默认为 SWOOLE_PROCESS
|
||||
];
|
||||
|
||||
/* 默认存取炸毛数据的目录(相对目录时,代表WORKING_DIR下的目录,绝对目录按照绝对目录来) */
|
||||
$config['data_dir'] = 'zm_data';
|
||||
|
||||
/* 框架本体运行时的一些可调配置 */
|
||||
$config['runtime'] = [
|
||||
'reload_delay_time' => 800,
|
||||
@ -57,4 +60,24 @@ $config['runtime'] = [
|
||||
'timezone' => 'Asia/Shanghai',
|
||||
];
|
||||
|
||||
/* 上下文接口类 implemented from ContextInterface */
|
||||
$config['context_class'] = \ZM\Context\Context::class;
|
||||
|
||||
/* 允许加载插件形式 */
|
||||
$config['plugin'] = [
|
||||
'enable' => true,
|
||||
'load_dir' => 'plugins',
|
||||
];
|
||||
|
||||
/* 静态文件读取器 */
|
||||
$config['file_server'] = [
|
||||
'enable' => true,
|
||||
'document_root' => $config['data_dir'] . '/public/',
|
||||
'document_index' => 'index.html',
|
||||
'document_code_page' => [
|
||||
'404' => '404.html',
|
||||
'500' => '500.html',
|
||||
],
|
||||
];
|
||||
|
||||
return $config;
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Swoole\OnOpenEvent;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Module\InstantModule;
|
||||
use ZM\ZMServer;
|
||||
|
||||
$weather = new InstantModule('weather');
|
||||
|
||||
$weather->onEvent(OnOpenEvent::class, ['connect_type' => 'qq'], function (ConnectionObject $conn) {
|
||||
Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已连接!');
|
||||
});
|
||||
|
||||
$weather->onEvent(CQCommand::class, ['match' => '你好'], function () {
|
||||
ctx()->reply('hello呀!');
|
||||
});
|
||||
|
||||
$app = new ZMServer('app-name');
|
||||
$app->addModule($weather);
|
||||
$app->run();
|
||||
22
instant-plugin-demo.php
Normal file
22
instant-plugin-demo.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
return function () {
|
||||
$plugin = new \ZM\Plugin\InstantPlugin(__DIR__);
|
||||
|
||||
$cmd = \ZM\Annotation\OneBot\BotCommand::make(name: 'test', match: '测试')->withArgument(name: 'arg1')->withMethod(function () {
|
||||
ctx()->reply('test ok');
|
||||
});
|
||||
$event = BotEvent::make(type: 'message')->withMethod(function () {
|
||||
});
|
||||
$plugin->addBotEvent($event);
|
||||
$plugin->addBotCommand($cmd);
|
||||
|
||||
$plugin->registerEvent(HttpRequestEvent::getName(), function (HttpRequestEvent $event) {
|
||||
$event->withResponse(\OneBot\Http\HttpFactory::getInstance()->createResponse(503));
|
||||
});
|
||||
return $plugin;
|
||||
};
|
||||
*/
|
||||
39
mybot.php
Normal file
39
mybot.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// 创建框架 App
|
||||
$app = new ZM\InstantApplication();
|
||||
// 传入自定义配置文件
|
||||
$app->patchConfig([
|
||||
'driver' => 'workerman',
|
||||
]);
|
||||
// 改变启动所需的参数
|
||||
$app->patchArgs([
|
||||
'--private-mode',
|
||||
]);
|
||||
// 如果有 Composer 依赖的插件,使用 enablePlugins 进行开启
|
||||
$app->enablePlugins([
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
]);
|
||||
// BotCommand 事件构造
|
||||
$cmd = \ZM\Annotation\OneBot\BotCommand::make('test')->withMethod(function () {
|
||||
ctx()->reply('test ok');
|
||||
});
|
||||
$event = \ZM\Annotation\OneBot\BotEvent::make('message')->withMethod(function () {
|
||||
});
|
||||
$app->addBotEvent($event);
|
||||
$app->addBotCommand($cmd);
|
||||
|
||||
$app->registerEvent(HttpRequestEvent::getName(), function (HttpRequestEvent $event) {
|
||||
$event->withResponse(\OneBot\Http\HttpFactory::getInstance()->createResponse(503));
|
||||
});
|
||||
|
||||
$app->run();
|
||||
@ -25,9 +25,11 @@ const ZM_PROCESS_WORKER = ONEBOT_PROCESS_WORKER;
|
||||
const ZM_PROCESS_USER = ONEBOT_PROCESS_USER;
|
||||
const ZM_PROCESS_TASKWORKER = ONEBOT_PROCESS_TASKWORKER;
|
||||
|
||||
const ZM_PARSE_BEFORE_DRIVER = 0;
|
||||
const ZM_PARSE_AFTER_DRIVER = 1;
|
||||
const ZM_PARSE_BEFORE_START = 2;
|
||||
/** 定义一些内部引用的错误ID */
|
||||
const ZM_ERR_NONE = 0; // 正常
|
||||
const ZM_ERR_METHOD_NOT_FOUND = 1; // 找不到方法
|
||||
const ZM_ERR_ROUTE_NOT_FOUND = 2; // 找不到路由
|
||||
const ZM_ERR_ROUTE_METHOD_NOT_ALLOWED = 3; // 路由方法不允许
|
||||
|
||||
/* 定义工作目录 */
|
||||
define('WORKING_DIR', getcwd());
|
||||
@ -52,7 +54,6 @@ if (DIRECTORY_SEPARATOR !== '\\') {
|
||||
|
||||
/* 对 global.php 在 Windows 下的兼容性考虑,因为 Windows 或者无 Swoole 环境时候无法运行 */
|
||||
!defined('SWOOLE_BASE') && define('SWOOLE_BASE', 1) && define('SWOOLE_PROCESS', 2);
|
||||
|
||||
!defined('SWOOLE_HOOK_ALL') && (
|
||||
define('SWOOLE_HOOK_TCP', 2)
|
||||
&& define('SWOOLE_HOOK_UDP', 4)
|
||||
|
||||
@ -2,11 +2,14 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** 定义炸毛框架初始启动时间 */
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
/* 定义炸毛框架初始启动时间 */
|
||||
if (!defined('ZM_START_TIME')) {
|
||||
define('ZM_START_TIME', microtime(true));
|
||||
}
|
||||
|
||||
/* 定义使用炸毛框架应用的版本 */
|
||||
if (!defined('APP_VERSION')) {
|
||||
define('APP_VERSION', LOAD_MODE == 1 ? (json_decode(file_get_contents(SOURCE_ROOT_DIR . '/composer.json'), true)['version'] ?? 'unknown') : 'unknown');
|
||||
define('APP_VERSION', LOAD_MODE == 1 ? (ZMUtil::getComposerMetadata()['version'] ?? ZM_VERSION) : ZM_VERSION);
|
||||
}
|
||||
|
||||
@ -2,8 +2,18 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ZM\Container\Container;
|
||||
use ZM\Container\ContainerInterface;
|
||||
use ZM\Context\Context;
|
||||
use ZM\Logger\ConsoleLogger;
|
||||
use ZM\Middleware\MiddlewareHandler;
|
||||
|
||||
// 防止重复引用引发报错
|
||||
if (function_exists('zm_internal_errcode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据具体操作系统替换目录分隔符
|
||||
@ -61,12 +71,66 @@ function is_assoc_array(array $array): bool
|
||||
return !empty($array) && array_keys($array) !== range(0, count($array) - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object
|
||||
*
|
||||
* TODO: 等待完善DI
|
||||
*/
|
||||
function resolve(string $class)
|
||||
function ctx(): Context
|
||||
{
|
||||
return new $class();
|
||||
return \container()->get('ctx');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建消息段的助手函数
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @param array $data 字段
|
||||
*/
|
||||
function segment(string $type, array $data = []): MessageSegment
|
||||
{
|
||||
return new MessageSegment($type, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间件操作类的助手函数
|
||||
*/
|
||||
function middleware(): MiddlewareHandler
|
||||
{
|
||||
return MiddlewareHandler::getInstance();
|
||||
}
|
||||
|
||||
// ////////////////// 容器部分 //////////////////////
|
||||
|
||||
/**
|
||||
* 获取容器(请求级)实例
|
||||
*/
|
||||
function container(): ContainerInterface
|
||||
{
|
||||
return Container::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析类实例(使用容器)
|
||||
*
|
||||
* @template T
|
||||
* @param class-string<T> $abstract
|
||||
* @return Closure|mixed|T
|
||||
* @noinspection PhpDocMissingThrowsInspection
|
||||
*/
|
||||
function resolve(string $abstract, array $parameters = [])
|
||||
{
|
||||
/* @noinspection PhpUnhandledExceptionInspection */
|
||||
return Container::getInstance()->make($abstract, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取容器实例
|
||||
*
|
||||
* @template T
|
||||
* @param null|class-string<T> $abstract
|
||||
* @return Closure|ContainerInterface|mixed|T
|
||||
*/
|
||||
function app(string $abstract = null, array $parameters = [])
|
||||
{
|
||||
if (is_null($abstract)) {
|
||||
return container();
|
||||
}
|
||||
|
||||
return resolve($abstract, $parameters);
|
||||
}
|
||||
|
||||
@ -2,60 +2,43 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use Koriym\Attributes\AttributeReader;
|
||||
use Koriym\Attributes\DualReader;
|
||||
use ZM\Annotation\Framework\OnSetup;
|
||||
use ZM\ConsoleApplication;
|
||||
use ZM\Exception\InitException;
|
||||
use ZM\Store\FileSystem;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Framework\Setup;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
function _zm_setup_loader()
|
||||
{
|
||||
try {
|
||||
try {
|
||||
new ConsoleApplication('zhamao');
|
||||
} catch (InitException $e) {
|
||||
}
|
||||
$base_path = SOURCE_ROOT_DIR;
|
||||
$scan_paths = [];
|
||||
$composer = json_decode(file_get_contents($base_path . '/composer.json'), true);
|
||||
$exclude_annotations = array_merge($composer['extra']['exclude_annotate'] ?? [], $composer['extra']['zm']['exclude-annotation-path'] ?? []);
|
||||
foreach (($composer['autoload']['psr-4'] ?? []) as $k => $v) {
|
||||
if (is_dir($base_path . '/' . $v) && !in_array($v, $exclude_annotations)) {
|
||||
$scan_paths[trim($k, '\\')] = $base_path . '/' . $v;
|
||||
global $_tmp_setup_list;
|
||||
$_tmp_setup_list = [];
|
||||
$parser = new AnnotationParser(false);
|
||||
$composer = ZMUtil::getComposerMetadata();
|
||||
// 合并 dev 和 非 dev 的 psr-4 加载目录
|
||||
$merge_psr4 = array_merge($composer['autoload']['psr-4'] ?? [], $composer['autoload-dev']['psr-4'] ?? []);
|
||||
// 排除 composer.json 中指定需要排除的目录
|
||||
$excludes = $composer['extra']['zm']['exclude-annotation-path'] ?? [];
|
||||
foreach ($merge_psr4 as $k => $v) {
|
||||
// 如果在排除表就排除,否则就解析注解
|
||||
if (is_dir(SOURCE_ROOT_DIR . '/' . $v) && !in_array($v, $excludes)) {
|
||||
// 添加解析路径,对应Base命名空间也贴出来
|
||||
$parser->addRegisterPath(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\'));
|
||||
}
|
||||
}
|
||||
foreach (($composer['autoload-dev']['psr-4'] ?? []) as $k => $v) {
|
||||
if (is_dir($base_path . '/' . $v) && !in_array($v, $exclude_annotations)) {
|
||||
$scan_paths[trim($k, '\\')] = $base_path . '/' . $v;
|
||||
}
|
||||
}
|
||||
$all_event_class = [];
|
||||
foreach ($scan_paths as $namespace => $autoload_path) {
|
||||
$all_event_class = array_merge($all_event_class, FileSystem::getClassesPsr4($autoload_path, $namespace));
|
||||
}
|
||||
$parser->addSpecialParser(Setup::class, function (Setup $setup) {
|
||||
global $_tmp_setup_list;
|
||||
$_tmp_setup_list[] = [
|
||||
'class' => $setup->class,
|
||||
'method' => $setup->method,
|
||||
];
|
||||
return true;
|
||||
});
|
||||
|
||||
$reader = new DualReader(new AnnotationReader(), new AttributeReader());
|
||||
$event_list = [];
|
||||
$setup_list = [];
|
||||
foreach ($all_event_class as $v) {
|
||||
$reflection_class = new ReflectionClass($v);
|
||||
$methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||
foreach ($methods as $vs) {
|
||||
$method_annotations = $reader->getMethodAnnotations($vs);
|
||||
if ($method_annotations != []) {
|
||||
$annotation = $method_annotations[0];
|
||||
if ($annotation instanceof OnSetup) {
|
||||
$setup_list[] = [
|
||||
'class' => $v,
|
||||
'method' => $vs->getName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return json_encode(['setup' => $setup_list, 'event' => $event_list]);
|
||||
// TODO: 然后加载插件目录下的插件
|
||||
|
||||
// 解析所有注册路径的文件,获取注解
|
||||
$parser->parseAll();
|
||||
|
||||
return json_encode(['setup' => $_tmp_setup_list]);
|
||||
} catch (Throwable $e) {
|
||||
$stderr = fopen('php://stderr', 'w');
|
||||
fwrite($stderr, zm_internal_errcode('E00031') . $e->getMessage() . ' in ' . $e->getFile() . ' at line ' . $e->getLine() . PHP_EOL);
|
||||
|
||||
26
src/Module/Example/Hello123.php
Normal file
26
src/Module/Example/Hello123.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\Framework\Setup;
|
||||
use ZM\Annotation\Http\Route;
|
||||
use ZM\Annotation\Middleware\Middleware;
|
||||
use ZM\Middleware\TimerMiddleware;
|
||||
|
||||
class Hello123
|
||||
{
|
||||
#[Setup]
|
||||
public function onRequest()
|
||||
{
|
||||
echo "OK\n";
|
||||
}
|
||||
|
||||
#[Route('/route', request_method: ['GET'])]
|
||||
#[Middleware(TimerMiddleware::class)]
|
||||
public function route()
|
||||
{
|
||||
return 'Hello Zhamao!This is the first 3.0 page!';
|
||||
}
|
||||
}
|
||||
@ -39,6 +39,17 @@ abstract class AnnotationBase implements IteratorAggregate
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 InstantPlugin 下调用,设置回调或匿名函数
|
||||
*
|
||||
* @param Closure|string $method
|
||||
*/
|
||||
public function withMethod($method): AnnotationBase
|
||||
{
|
||||
$this->method = $method;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator($this);
|
||||
|
||||
@ -4,10 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZM\Annotation;
|
||||
|
||||
use Generator;
|
||||
use Throwable;
|
||||
use ZM\Annotation\Middleware\Middleware;
|
||||
use ZM\Exception\InterruptException;
|
||||
use ZM\Middleware\MiddlewareHandler;
|
||||
|
||||
/**
|
||||
* 注解调用器,原 EventDispatcher
|
||||
@ -39,17 +38,34 @@ class AnnotationHandler
|
||||
/** @var mixed */
|
||||
private $return_val;
|
||||
|
||||
/**
|
||||
* 注解调用器构造函数
|
||||
*
|
||||
* @param string $annotation_class 注解类名
|
||||
*/
|
||||
public function __construct(string $annotation_class)
|
||||
{
|
||||
$this->annotation_class = $annotation_class;
|
||||
logger()->debug('开始分发注解 {annotation}', ['annotation' => $annotation_class]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 立刻中断注解调用器执行
|
||||
*
|
||||
* @param mixed $return_var 中断执行返回值,传入null则代表无返回值
|
||||
* @throws InterruptException
|
||||
*/
|
||||
public static function interrupt($return_var = null)
|
||||
{
|
||||
throw new InterruptException($return_var);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置执行前判断注解是否应该被执行的检查回调函数
|
||||
*
|
||||
* @param callable $rule 回调函数
|
||||
* @return $this
|
||||
*/
|
||||
public function setRuleCallback(callable $rule): AnnotationHandler
|
||||
{
|
||||
logger()->debug('注解调用器设置事件ruleFunc: {annotation}', ['annotation' => $this->annotation_class]);
|
||||
@ -57,6 +73,12 @@ class AnnotationHandler
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置成功执行后有返回值时执行的返回值后续逻辑回调函数
|
||||
*
|
||||
* @param callable $return 回调函数
|
||||
* @return $this
|
||||
*/
|
||||
public function setReturnCallback(callable $return): AnnotationHandler
|
||||
{
|
||||
logger()->debug('注解调用器设置事件returnFunc: {annotation}', ['annotation' => $this->annotation_class]);
|
||||
@ -65,119 +87,97 @@ class AnnotationHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed ...$params
|
||||
* 调用注册了该注解的所有函数们
|
||||
* 此处会遍历所有注册了当前注解的函数,并支持中间件插入
|
||||
*
|
||||
* @param mixed ...$params 传入的参数们
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function handleAll(...$params)
|
||||
{
|
||||
try {
|
||||
// 遍历注册的注解
|
||||
foreach ((AnnotationMap::$_list[$this->annotation_class] ?? []) as $v) {
|
||||
// 调用单个注解
|
||||
$this->handle($v, $this->rule_callback, ...$params);
|
||||
// 执行完毕后检查状态,如果状态是规则判断或中间件before不通过,则重置状态后继续执行别的注解函数
|
||||
if ($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) {
|
||||
$this->status = self::STATUS_NORMAL;
|
||||
continue;
|
||||
}
|
||||
// 如果执行完毕,且设置了返回值后续逻辑的回调函数,那么就调用返回值回调的逻辑
|
||||
if (is_callable($this->return_callback) && $this->status === self::STATUS_NORMAL) {
|
||||
($this->return_callback)($this->return_val);
|
||||
}
|
||||
}
|
||||
} catch (InterruptException $e) {
|
||||
// InterruptException 用于中断,这里必须 catch,并标记状态
|
||||
$this->return_val = $e->return_var;
|
||||
$this->status = self::STATUS_INTERRUPTED;
|
||||
} catch (Throwable $e) {
|
||||
// 其他类型的异常就顺势再抛出到外层,此层不做处理
|
||||
$this->status = self::STATUS_EXCEPTION;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用单个注解
|
||||
*
|
||||
* @param mixed ...$params 传入的参数们
|
||||
* @throws InterruptException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function handle(AnnotationBase $v, ?callable $rule_callback = null, ...$params): bool
|
||||
{
|
||||
$target_class = resolve($v->class);
|
||||
// 由于3.0有额外的插件模式支持,所以注解就不再提供独立的闭包函数调用支持了
|
||||
// 提取要调用的目标类和方法名称
|
||||
$target_class = new ($v->class)();
|
||||
$target_method = $v->method;
|
||||
// 先执行规则
|
||||
if ($rule_callback !== null && !$rule_callback($this, $params)) {
|
||||
// 先执行规则,失败就返回false
|
||||
if ($rule_callback !== null && !$rule_callback($v, $params)) {
|
||||
$this->status = self::STATUS_RULE_FAILED;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查中间件
|
||||
$mid_obj = [];
|
||||
$before_result = true;
|
||||
foreach ($this->getRegisteredMiddlewares($target_class, $target_method) as $v) {
|
||||
$mid_obj[] = $v[0]; // 投喂中间件
|
||||
if ($v[1] !== '') { // 顺带执行before
|
||||
if (function_exists('container')) {
|
||||
$before_result = container()->call([$v[0], $v[1]], $params);
|
||||
} else {
|
||||
$before_result = call_user_func([$v[0], $v[1]], $params);
|
||||
}
|
||||
if ($before_result === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$mid_obj_cnt1 = count($mid_obj) - 1;
|
||||
if ($before_result) { // before全部通过了
|
||||
try {
|
||||
// 执行注解绑定的方法
|
||||
// TODO: 记得完善好容器后把这里的这个if else去掉
|
||||
if (function_exists('container')) {
|
||||
$this->return_val = container()->call([$target_class, $target_method], $params);
|
||||
} else {
|
||||
$this->return_val = call_user_func([$target_class, $target_method], $params);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
if ($e instanceof InterruptException) {
|
||||
throw $e;
|
||||
}
|
||||
for ($i = $mid_obj_cnt1; $i >= 0; --$i) {
|
||||
$obj = $mid_obj[$i];
|
||||
foreach ($obj[3] as $name => $method) {
|
||||
if ($e instanceof $name) {
|
||||
$obj[0]->{$method}($e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
$this->status = self::STATUS_BEFORE_FAILED;
|
||||
}
|
||||
for ($i = $mid_obj_cnt1; $i >= 0; --$i) {
|
||||
if ($mid_obj[$i][2] !== '') {
|
||||
$mid_obj[$i][0]->{$mid_obj[$i][2]}($this->return_val);
|
||||
$callback = [$target_class, $target_method];
|
||||
try {
|
||||
// 这块代码几乎等同于 middleware()->process() 中的内容,但由于注解调用器内含有一些特殊的特性(比如返回值回调),所以需要拆开来
|
||||
$before_result = middleware()->processBefore($callback, $params);
|
||||
if ($before_result) {
|
||||
// before都通过了,就执行本身,通过依赖注入执行
|
||||
// $this->return_val = container()->call($callback, $params);
|
||||
$this->return_val = $callback(...$params);
|
||||
} else {
|
||||
// 没通过就标记是BEFORE_FAILED,然后接着执行after
|
||||
$this->status = self::STATUS_BEFORE_FAILED;
|
||||
}
|
||||
middleware()->processAfter($callback, $params);
|
||||
} /* @noinspection PhpRedundantCatchClauseInspection */ catch (InterruptException $e) {
|
||||
// 这里直接抛出这个异常的目的就是给上层handleAll()捕获
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
// 其余的异常就交给中间件的异常捕获器过一遍,没捕获的则继续抛出
|
||||
$this->status = self::STATUS_EXCEPTION;
|
||||
MiddlewareHandler::getInstance()->processException($callback, $params, $e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册过的中间件
|
||||
*
|
||||
* @param object|string $class 类对象
|
||||
* @param string $method 方法名称
|
||||
* 获取分发的状态
|
||||
*/
|
||||
private function getRegisteredMiddlewares($class, string $method): Generator
|
||||
public function getStatus(): int
|
||||
{
|
||||
foreach (AnnotationMap::$_map[get_class($class)][$method] ?? [] as $annotation) {
|
||||
if ($annotation instanceof Middleware) {
|
||||
$name = $annotation->name;
|
||||
$reg_mid = AnnotationMap::$_middleware_map[$name]['class'] ?? null;
|
||||
if ($reg_mid === null) {
|
||||
logger()->error('Not a valid middleware name: {name}', ['name' => $name]);
|
||||
continue;
|
||||
}
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
$obj = new $reg_mid($annotation->params);
|
||||
yield [
|
||||
$obj,
|
||||
AnnotationMap::$_middleware_map[$name]['before'] ?? '',
|
||||
AnnotationMap::$_middleware_map[$name]['after'] ?? '',
|
||||
AnnotationMap::$_middleware_map[$name]['exceptions'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
/**
|
||||
* 获取运行的返回值
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getReturnVal()
|
||||
{
|
||||
return $this->return_val;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ namespace ZM\Annotation;
|
||||
class AnnotationMap
|
||||
{
|
||||
/**
|
||||
* 存取注解对象的列表
|
||||
* 存取注解对象的列表,key是注解类名,value是该注解对应的数组
|
||||
*
|
||||
* @var array<string, array<AnnotationBase>>
|
||||
* @internal
|
||||
@ -18,14 +18,22 @@ class AnnotationMap
|
||||
public static $_list = [];
|
||||
|
||||
/**
|
||||
* 存取注解对象的三维列表,key1是注解所在的类名,key2是注解所在的方法名,value是该方法标注的注解们(数组)
|
||||
*
|
||||
* @var array<string, array<string, array<AnnotationBase>>>
|
||||
* @internal
|
||||
*/
|
||||
public static $_map = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @internal
|
||||
* 将Parser解析后的注解注册到全局的 AnnotationMap
|
||||
*
|
||||
* @param AnnotationParser $parser 注解解析器
|
||||
*/
|
||||
public static $_middleware_map = [];
|
||||
public static function loadAnnotationByParser(AnnotationParser $parser)
|
||||
{
|
||||
// 生成后加入到全局list中
|
||||
self::$_list = array_merge(self::$_list, $parser->generateAnnotationList());
|
||||
self::$_map = $parser->getAnnotationMap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,64 +10,90 @@ use Koriym\Attributes\DualReader;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
use ZM\Annotation\Http\Controller;
|
||||
use ZM\Annotation\Http\Route;
|
||||
use ZM\Annotation\Interfaces\ErgodicAnnotation;
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
use ZM\Annotation\Middleware\HandleAfter;
|
||||
use ZM\Annotation\Middleware\HandleBefore;
|
||||
use ZM\Annotation\Middleware\HandleException;
|
||||
use ZM\Annotation\Middleware\Middleware;
|
||||
use ZM\Annotation\Middleware\MiddlewareClass;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Store\FileSystem;
|
||||
use ZM\Store\InternalGlobals;
|
||||
use ZM\Utils\HttpUtil;
|
||||
|
||||
/**
|
||||
* 注解解析器
|
||||
*/
|
||||
class AnnotationParser
|
||||
{
|
||||
/**
|
||||
* @var array 要解析的路径列表
|
||||
*/
|
||||
private $path_list = [];
|
||||
|
||||
/**
|
||||
* @var float 用于计算解析时间用的
|
||||
*/
|
||||
private $start_time;
|
||||
|
||||
/**
|
||||
* @var array 用于解析的注解解析树,格式见下方的注释
|
||||
*/
|
||||
private $annotation_tree = [];
|
||||
|
||||
/**
|
||||
* @var array 用于生成"类-方法"对应"注解列表"的数组
|
||||
*/
|
||||
private $annotation_map = [];
|
||||
|
||||
private $middleware_map = [];
|
||||
|
||||
private $middlewares = [];
|
||||
|
||||
/** @var null|AnnotationReader|DualReader */
|
||||
private $reader;
|
||||
|
||||
private $req_mapping = [];
|
||||
/**
|
||||
* @var array 特殊的注解解析器回调列表
|
||||
*/
|
||||
private $special_parsers = [];
|
||||
|
||||
/**
|
||||
* AnnotationParser constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
public function __construct(bool $with_internal_parsers = true)
|
||||
{
|
||||
$this->start_time = microtime(true);
|
||||
// $this->loadAnnotationClasses();
|
||||
$this->req_mapping[0] = [
|
||||
'id' => 0,
|
||||
'pid' => -1,
|
||||
'name' => '/',
|
||||
];
|
||||
|
||||
if ($with_internal_parsers) {
|
||||
$this->special_parsers = [
|
||||
Middleware::class => [function (Middleware $middleware) { \middleware()->bindMiddleware([resolve($middleware->class), $middleware->method], $middleware->name, $middleware->params); }],
|
||||
Route::class => [[$this, 'addRouteAnnotation']],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义的注解解析方法
|
||||
*
|
||||
* @param string $class_name 注解类名
|
||||
* @param callable $callback 回调函数
|
||||
*/
|
||||
public function addSpecialParser(string $class_name, callable $callback)
|
||||
{
|
||||
$this->special_parsers[$class_name][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册各个模块类的注解和模块level的排序
|
||||
*
|
||||
* @throws ReflectionException
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public function parseAll()
|
||||
{
|
||||
// 对每个设置的路径依次解析
|
||||
foreach ($this->path_list as $path) {
|
||||
logger()->debug('parsing annotation in ' . $path[0] . ':' . $path[1]);
|
||||
|
||||
// 首先获取路径下所有的类(通过 PSR-4 标准解析)
|
||||
$all_class = FileSystem::getClassesPsr4($path[0], $path[1]);
|
||||
|
||||
// 读取配置文件中配置的忽略解析的注解名,防止误解析一些别的地方需要的注解,比如@mixin
|
||||
$conf = ZMConfig::get('global.runtime.annotation_reader_ignore');
|
||||
// 有两种方式,第一种是通过名称,第二种是通过命名空间
|
||||
if (isset($conf['name']) && is_array($conf['name'])) {
|
||||
foreach ($conf['name'] as $v) {
|
||||
AnnotationReader::addGlobalIgnoredName($v);
|
||||
@ -78,14 +104,18 @@ class AnnotationParser
|
||||
AnnotationReader::addGlobalIgnoredNamespace($v);
|
||||
}
|
||||
}
|
||||
// 因为mixin常用,且框架默认不需要解析,则全局忽略
|
||||
AnnotationReader::addGlobalIgnoredName('mixin');
|
||||
$this->reader = new DualReader(new AnnotationReader(), new AttributeReader());
|
||||
|
||||
// 声明一个既可以解析注解又可以解析Attribute的双reader来读取注解和Attribute
|
||||
$reader = new DualReader(new AnnotationReader(), new AttributeReader());
|
||||
foreach ($all_class as $v) {
|
||||
logger()->debug('正在检索 ' . $v);
|
||||
|
||||
// 通过反射实现注解读取
|
||||
$reflection_class = new ReflectionClass($v);
|
||||
$methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||
$class_annotations = $this->reader->getClassAnnotations($reflection_class);
|
||||
$class_annotations = $reader->getClassAnnotations($reflection_class);
|
||||
// 这段为新加的:start
|
||||
// 这里将每个类里面所有的类注解、方法注解通通加到一颗大树上,后期解析
|
||||
/*
|
||||
@ -105,64 +135,85 @@ class AnnotationParser
|
||||
}
|
||||
*/
|
||||
|
||||
// 生成主树
|
||||
$this->annotation_map[$v]['class_annotations'] = $class_annotations;
|
||||
$this->annotation_map[$v]['methods'] = $methods;
|
||||
// 保存对class的注解
|
||||
$this->annotation_tree[$v]['class_annotations'] = $class_annotations;
|
||||
// 保存类成员的方法的对应反射对象们
|
||||
$this->annotation_tree[$v]['methods'] = $methods;
|
||||
// 保存对每个方法获取到的注解们
|
||||
foreach ($methods as $method) {
|
||||
$this->annotation_map[$v]['methods_annotations'][$method->getName()] = $this->reader->getMethodAnnotations($method);
|
||||
$this->annotation_tree[$v]['methods_annotations'][$method->getName()] = $reader->getMethodAnnotations($method);
|
||||
}
|
||||
|
||||
foreach ($this->annotation_map[$v]['class_annotations'] as $vs) {
|
||||
// 因为适用于类的注解有一些比较特殊,比如有向下注入的,有控制行为的,所以需要遍历一下下放到方法里
|
||||
foreach ($this->annotation_tree[$v]['class_annotations'] as $vs) {
|
||||
$vs->class = $v;
|
||||
|
||||
// 预处理1:将适用于每一个函数的注解到类注解重新注解到每个函数下面
|
||||
if (($vs instanceof ErgodicAnnotation) && ($vs instanceof AnnotationBase)) {
|
||||
foreach (($this->annotation_map[$v]['methods'] ?? []) as $method) {
|
||||
// 预处理0:排除所有非继承于 AnnotationBase 的注解
|
||||
if (!$vs instanceof AnnotationBase) {
|
||||
logger()->notice(get_class($vs) . ' is not extended from ' . AnnotationBase::class);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 预处理1:如果类包含了@Closed注解,则跳过这个类
|
||||
if ($vs instanceof Closed) {
|
||||
unset($this->annotation_tree[$v]);
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// 预处理2:将适用于每一个函数的注解到类注解重新注解到每个函数下面
|
||||
if ($vs instanceof ErgodicAnnotation) {
|
||||
foreach (($this->annotation_tree[$v]['methods'] ?? []) as $method) {
|
||||
// 用 clone 的目的是生成个独立的对象,避免和 class 以及方法之间互相冲突
|
||||
$copy = clone $vs;
|
||||
$copy->method = $method->getName();
|
||||
$this->annotation_map[$v]['methods_annotations'][$method->getName()][] = $copy;
|
||||
$this->annotation_tree[$v]['methods_annotations'][$method->getName()][] = $copy;
|
||||
}
|
||||
}
|
||||
|
||||
// 预处理2:处理 class 下面的注解
|
||||
if ($vs instanceof Closed) {
|
||||
unset($this->annotation_map[$v]);
|
||||
continue 2;
|
||||
}
|
||||
if ($vs instanceof MiddlewareClass) {
|
||||
// 注册中间件本身的类,标记到 middlewares 属性中
|
||||
logger()->debug('正在注册中间件 ' . $reflection_class->getName());
|
||||
$rs = $this->registerMiddleware($vs, $reflection_class);
|
||||
$this->middlewares[$rs['name']] = $rs;
|
||||
// 预处理3:调用自定义解析器
|
||||
foreach (($this->special_parsers[get_class($vs)] ?? []) as $parser) {
|
||||
$result = $parser($vs);
|
||||
if ($result === true) {
|
||||
continue 2;
|
||||
}
|
||||
if ($result === false) {
|
||||
continue 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$inserted = [];
|
||||
|
||||
// 预处理3:处理每个函数上面的特殊注解,就是需要操作一些东西的
|
||||
foreach (($this->annotation_map[$v]['methods_annotations'] ?? []) as $method_name => $methods_annotations) {
|
||||
foreach (($this->annotation_tree[$v]['methods_annotations'] ?? []) as $method_name => $methods_annotations) {
|
||||
foreach ($methods_annotations as $method_anno) {
|
||||
/* @var AnnotationBase $method_anno */
|
||||
// 预处理3.0:排除所有非继承于 AnnotationBase 的注解
|
||||
if (!$method_anno instanceof AnnotationBase) {
|
||||
logger()->notice('Binding annotation ' . get_class($method_anno) . ' to ' . $v . '::' . $method_name . ' is not extended from ' . AnnotationBase::class);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 预处理3.1:给所有注解对象绑定当前的类名和方法名
|
||||
$method_anno->class = $v;
|
||||
$method_anno->method = $method_name;
|
||||
if (!($method_anno instanceof Middleware) && ($middlewares = ZMConfig::get('global.global_middleware_binding')[get_class($method_anno)] ?? []) !== []) {
|
||||
if (!isset($inserted[$v][$method_name])) {
|
||||
// 在这里在其他中间件前插入插入全局的中间件
|
||||
foreach ($middlewares as $middleware) {
|
||||
$mid_class = new Middleware($middleware);
|
||||
$mid_class->class = $v;
|
||||
$mid_class->method = $method_name;
|
||||
$this->middleware_map[$v][$method_name][] = $mid_class;
|
||||
}
|
||||
$inserted[$v][$method_name] = true;
|
||||
}
|
||||
} elseif ($method_anno instanceof Route) {
|
||||
$this->addRouteAnnotation($method_anno, $method_name, $v, $methods_annotations);
|
||||
} elseif ($method_anno instanceof Middleware) {
|
||||
$this->middleware_map[$method_anno->class][$method_anno->method][] = $method_anno;
|
||||
} else {
|
||||
AnnotationMap::$_map[$method_anno->class][$method_anno->method][] = $method_anno;
|
||||
|
||||
// 预处理3.2:如果包含了@Closed注解,则跳过这个方法的注解解析
|
||||
if ($method_anno instanceof Closed) {
|
||||
unset($this->annotation_tree[$v]['methods_annotations'][$method_name]);
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// 预处理3.3:调用自定义解析器
|
||||
foreach (($this->special_parsers[get_class($method_anno)] ?? []) as $parser) {
|
||||
$result = $parser($method_anno);
|
||||
if ($result === true) {
|
||||
continue 2;
|
||||
}
|
||||
if ($result === false) {
|
||||
continue 3;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果上方没有解析或返回了 true,则添加到注解解析列表中
|
||||
$this->annotation_map[$v][$method_name][] = $method_anno;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,10 +221,13 @@ class AnnotationParser
|
||||
logger()->debug('解析注解完毕!');
|
||||
}
|
||||
|
||||
public function generateAnnotationEvents(): array
|
||||
/**
|
||||
* 生成排序后的注解列表
|
||||
*/
|
||||
public function generateAnnotationList(): array
|
||||
{
|
||||
$o = [];
|
||||
foreach ($this->annotation_map as $obj) {
|
||||
foreach ($this->annotation_tree as $obj) {
|
||||
// 这里的ErgodicAnnotation是为了解决类上的注解可穿透到方法上的问题
|
||||
foreach (($obj['class_annotations'] ?? []) as $class_annotation) {
|
||||
if ($class_annotation instanceof ErgodicAnnotation) {
|
||||
@ -193,22 +247,9 @@ class AnnotationParser
|
||||
return $o;
|
||||
}
|
||||
|
||||
public function getMiddlewares(): array
|
||||
{
|
||||
return $this->middlewares;
|
||||
}
|
||||
|
||||
public function getMiddlewareMap(): array
|
||||
{
|
||||
return $this->middleware_map;
|
||||
}
|
||||
|
||||
public function getReqMapping(): array
|
||||
{
|
||||
return $this->req_mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加解析的路径
|
||||
*
|
||||
* @param string $path 注册解析注解的路径
|
||||
* @param string $indoor_name 起始命名空间的名称
|
||||
*/
|
||||
@ -219,6 +260,8 @@ class AnnotationParser
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序注解列表
|
||||
*
|
||||
* @param array $events 需要排序的
|
||||
* @param string $class_name 排序的类名
|
||||
* @param string $prefix 前缀
|
||||
@ -229,53 +272,37 @@ class AnnotationParser
|
||||
if (is_a($class_name, Level::class, true)) {
|
||||
$class_name .= $prefix;
|
||||
usort($events[$class_name], function ($a, $b) {
|
||||
$left = $a->level;
|
||||
$right = $b->level;
|
||||
$left = $a->getLevel();
|
||||
$right = $b->getLevel();
|
||||
return $left > $right ? -1 : ($left == $right ? 0 : 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function getUsedTime()
|
||||
/**
|
||||
* 获取解析器调用的时间(秒)
|
||||
*/
|
||||
public function getUsedTime(): float
|
||||
{
|
||||
return microtime(true) - $this->start_time;
|
||||
}
|
||||
|
||||
// private function below
|
||||
|
||||
private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class): array
|
||||
/**
|
||||
* 获取注解的注册map
|
||||
*/
|
||||
public function getAnnotationMap(): array
|
||||
{
|
||||
$result = [
|
||||
'class' => '\\' . $reflection_class->getName(),
|
||||
'name' => $vs->name,
|
||||
];
|
||||
|
||||
foreach ($reflection_class->getMethods() as $vss) {
|
||||
$method_annotations = $this->reader->getMethodAnnotations($vss);
|
||||
foreach ($method_annotations as $vsss) {
|
||||
if ($vsss instanceof HandleBefore) {
|
||||
$result['before'] = $vss->getName();
|
||||
}
|
||||
if ($vsss instanceof HandleAfter) {
|
||||
$result['after'] = $vss->getName();
|
||||
}
|
||||
if ($vsss instanceof HandleException) {
|
||||
$result['exceptions'][$vsss->class_name] = $vss->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
return $this->annotation_map;
|
||||
}
|
||||
|
||||
private function addRouteAnnotation(Route $vss, $method, $class, $methods_annotations)
|
||||
/**
|
||||
* 添加注解路由
|
||||
*/
|
||||
private function addRouteAnnotation(Route $vss)
|
||||
{
|
||||
if (InternalGlobals::$routes === null) {
|
||||
InternalGlobals::$routes = new RouteCollection();
|
||||
}
|
||||
|
||||
// 拿到所属方法的类上面有没有控制器的注解
|
||||
$prefix = '';
|
||||
foreach ($methods_annotations as $annotation) {
|
||||
foreach (($this->annotation_tree[$vss->class]['methods_annotations'][$vss->method] ?? []) as $annotation) {
|
||||
if ($annotation instanceof Controller) {
|
||||
$prefix = $annotation->prefix;
|
||||
break;
|
||||
@ -284,9 +311,9 @@ class AnnotationParser
|
||||
$tail = trim($vss->route, '/');
|
||||
$route_name = $prefix . ($tail === '' ? '' : '/') . $tail;
|
||||
logger()->debug('添加路由:' . $route_name);
|
||||
$route = new \Symfony\Component\Routing\Route($route_name, ['_class' => $class, '_method' => $method]);
|
||||
$route = new \Symfony\Component\Routing\Route($route_name, ['_class' => $vss->class, '_method' => $vss->method]);
|
||||
$route->setMethods($vss->request_method);
|
||||
|
||||
InternalGlobals::$routes->add(md5($route_name), $route);
|
||||
HttpUtil::getRouteCollection()->add(md5($route_name), $route);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,9 +12,9 @@ use Doctrine\Common\Annotations\Annotation\Target;
|
||||
* Class Closed
|
||||
* @Annotation
|
||||
* @NamedArgumentConstructor()
|
||||
* @Target("CLASS")
|
||||
* @Target("ALL")
|
||||
*/
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
|
||||
class Closed extends AnnotationBase
|
||||
{
|
||||
}
|
||||
|
||||
53
src/ZM/Annotation/Framework/BindEvent.php
Normal file
53
src/ZM/Annotation/Framework/BindEvent.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Annotation\Framework;
|
||||
|
||||
use Attribute;
|
||||
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
|
||||
use Doctrine\Common\Annotations\Annotation\Required;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
|
||||
/**
|
||||
* Class BindEvent
|
||||
* 通过注解绑定 EventProvider 支持的事件
|
||||
*
|
||||
* @Annotation
|
||||
* @NamedArgumentConstructor()
|
||||
* @Target("METHOD")
|
||||
* @since 3.0.0
|
||||
*/
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
|
||||
class BindEvent extends AnnotationBase implements Level
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @Required()
|
||||
*/
|
||||
public $event_class;
|
||||
|
||||
/** @var int */
|
||||
public $level = 800;
|
||||
|
||||
/**
|
||||
* @param string $event_class 绑定事件的类型
|
||||
*/
|
||||
public function __construct(string $event_class, int $level = 800)
|
||||
{
|
||||
$this->event_class = $event_class;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function getLevel(): int
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
public function setLevel($level)
|
||||
{
|
||||
$this->level = $level;
|
||||
}
|
||||
}
|
||||
29
src/ZM/Annotation/Framework/Init.php
Normal file
29
src/ZM/Annotation/Framework/Init.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Annotation\Framework;
|
||||
|
||||
use Attribute;
|
||||
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
|
||||
/**
|
||||
* Class Init
|
||||
* @Annotation
|
||||
* @NamedArgumentConstructor()
|
||||
* @Target("METHOD")
|
||||
* @since 3.0.0
|
||||
*/
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
|
||||
class Init extends AnnotationBase
|
||||
{
|
||||
/** @var int */
|
||||
public $worker = 0;
|
||||
|
||||
public function __construct(int $worker = 0)
|
||||
{
|
||||
$this->worker = $worker;
|
||||
}
|
||||
}
|
||||
@ -14,8 +14,9 @@ use ZM\Annotation\AnnotationBase;
|
||||
* @Annotation
|
||||
* @NamedArgumentConstructor()
|
||||
* @Target("METHOD")
|
||||
* @since 3.0.0
|
||||
*/
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
|
||||
class OnSetup extends AnnotationBase
|
||||
class Setup extends AnnotationBase
|
||||
{
|
||||
}
|
||||
@ -48,4 +48,9 @@ class Route extends AnnotationBase
|
||||
$this->request_method = $request_method;
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
public static function make($route, $name = '', $request_method = ['GET', 'POST'], $params = [])
|
||||
{
|
||||
return new static($route, $name, $request_method, $params);
|
||||
}
|
||||
}
|
||||
|
||||
146
src/ZM/Annotation/OneBot/BotCommand.php
Normal file
146
src/ZM/Annotation/OneBot/BotCommand.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Annotation\OneBot;
|
||||
|
||||
use Attribute;
|
||||
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
use ZM\Exception\InvalidArgumentException;
|
||||
use ZM\Exception\ZMKnownException;
|
||||
|
||||
/**
|
||||
* Class BotCommand
|
||||
* 机器人指令注解
|
||||
*
|
||||
* @Annotation
|
||||
* @NamedArgumentConstructor()
|
||||
* @Target("METHOD")
|
||||
*/
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
|
||||
class BotCommand extends AnnotationBase implements Level
|
||||
{
|
||||
/** @var string */
|
||||
public $name = '';
|
||||
|
||||
/** @var string */
|
||||
public $match = '';
|
||||
|
||||
/** @var string */
|
||||
public $pattern = '';
|
||||
|
||||
/** @var string */
|
||||
public $regex = '';
|
||||
|
||||
/** @var string */
|
||||
public $start_with = '';
|
||||
|
||||
/** @var string */
|
||||
public $end_with = '';
|
||||
|
||||
/** @var string */
|
||||
public $keyword = '';
|
||||
|
||||
/** @var string[] */
|
||||
public $alias = [];
|
||||
|
||||
/** @var string */
|
||||
public $message_type = '';
|
||||
|
||||
/** @var string */
|
||||
public $user_id = '';
|
||||
|
||||
/** @var string */
|
||||
public $group_id = '';
|
||||
|
||||
/** @var int */
|
||||
public $level = 20;
|
||||
|
||||
/** @var array */
|
||||
private $arguments = [];
|
||||
|
||||
public function __construct(
|
||||
$name = '',
|
||||
$match = '',
|
||||
$pattern = '',
|
||||
$regex = '',
|
||||
$start_with = '',
|
||||
$end_with = '',
|
||||
$keyword = '',
|
||||
$alias = [],
|
||||
$message_type = '',
|
||||
$user_id = '',
|
||||
$group_id = '',
|
||||
$level = 20
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->match = $match;
|
||||
$this->pattern = $pattern;
|
||||
$this->regex = $regex;
|
||||
$this->start_with = $start_with;
|
||||
$this->end_with = $end_with;
|
||||
$this->keyword = $keyword;
|
||||
$this->alias = $alias;
|
||||
$this->message_type = $message_type;
|
||||
$this->user_id = $user_id;
|
||||
$this->group_id = $group_id;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public static function make(
|
||||
$name = '',
|
||||
$match = '',
|
||||
$pattern = '',
|
||||
$regex = '',
|
||||
$start_with = '',
|
||||
$end_with = '',
|
||||
$keyword = '',
|
||||
$alias = [],
|
||||
$message_type = '',
|
||||
$user_id = '',
|
||||
$group_id = '',
|
||||
$level = 20
|
||||
): BotCommand {
|
||||
return new static(...func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
* @throws ZMKnownException
|
||||
* @return $this
|
||||
*/
|
||||
public function withArgument(
|
||||
string $name,
|
||||
string $description = '',
|
||||
string $type = 'string',
|
||||
bool $required = false,
|
||||
string $prompt = '',
|
||||
string $default = '',
|
||||
int $timeout = 60,
|
||||
int $error_prompt_policy = 1
|
||||
): BotCommand {
|
||||
$this->arguments[] = new CommandArgument($name, $description, $type, $required, $prompt, $default, $timeout, $error_prompt_policy);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLevel(): int
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $level
|
||||
*/
|
||||
public function setLevel($level)
|
||||
{
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
}
|
||||
@ -10,12 +10,14 @@ use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
|
||||
/**
|
||||
* 机器人相关事件注解
|
||||
*
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
* @NamedArgumentConstructor
|
||||
* @NamedArgumentConstructor()
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class OnOneBotEvent extends AnnotationBase
|
||||
class BotEvent extends AnnotationBase
|
||||
{
|
||||
/** @var null|string */
|
||||
public $type;
|
||||
@ -50,4 +52,15 @@ class OnOneBotEvent extends AnnotationBase
|
||||
$this->self_id = $self_id;
|
||||
$this->sub_type = $sub_type;
|
||||
}
|
||||
|
||||
public static function make(
|
||||
?string $type = null,
|
||||
?string $detail_type = null,
|
||||
?string $impl = null,
|
||||
?string $platform = null,
|
||||
?string $self_id = null,
|
||||
?string $sub_type = null
|
||||
): BotEvent {
|
||||
return new static(...func_get_args());
|
||||
}
|
||||
}
|
||||
146
src/ZM/Annotation/OneBot/CommandArgument.php
Normal file
146
src/ZM/Annotation/OneBot/CommandArgument.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Annotation\OneBot;
|
||||
|
||||
use Attribute;
|
||||
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
|
||||
use Doctrine\Common\Annotations\Annotation\Required;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
use ZM\Annotation\Interfaces\ErgodicAnnotation;
|
||||
use ZM\Exception\InvalidArgumentException;
|
||||
use ZM\Exception\ZMKnownException;
|
||||
|
||||
/**
|
||||
* Class CommandArgument
|
||||
* @Annotation
|
||||
* @NamedArgumentConstructor()
|
||||
* @Target("ALL")
|
||||
*/
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
|
||||
class CommandArgument extends AnnotationBase implements ErgodicAnnotation
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @Required()
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $type = 'string';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $required = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $prompt = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $default = '';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $timeout = 60;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $error_prompt_policy = 1;
|
||||
|
||||
/**
|
||||
* @param string $name 参数名称(可以是中文)
|
||||
* @param string $description 参数描述(默认为空)
|
||||
* @param bool $required 参数是否必需,如果是必需,为true(默认为false)
|
||||
* @param string $prompt 当参数为必需时,返回给用户的提示输入的消息(默认为"请输入$name")
|
||||
* @param string $default 当required为false时,未匹配到参数将自动使用default值(默认为空)
|
||||
* @param int $timeout prompt超时时间(默认为60秒)
|
||||
* @throws InvalidArgumentException|ZMKnownException
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $description = '',
|
||||
string $type = 'string',
|
||||
bool $required = false,
|
||||
string $prompt = '',
|
||||
string $default = '',
|
||||
int $timeout = 60,
|
||||
int $error_prompt_policy = 1
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->description = $description;
|
||||
$this->type = $this->fixTypeName($type);
|
||||
$this->required = $required;
|
||||
$this->prompt = $prompt;
|
||||
$this->default = $default;
|
||||
$this->timeout = $timeout;
|
||||
$this->error_prompt_policy = $error_prompt_policy;
|
||||
if ($this->type === 'bool') {
|
||||
if ($this->default === '') {
|
||||
$this->default = 'yes';
|
||||
}
|
||||
if (!in_array($this->default, array_merge(TRUE_LIST, FALSE_LIST))) {
|
||||
throw new InvalidArgumentException('CommandArgument参数 ' . $name . ' 类型传入类型应为布尔型,检测到非法的默认值 ' . $this->default);
|
||||
}
|
||||
} elseif ($this->type === 'number') {
|
||||
if ($this->default === '') {
|
||||
$this->default = '0';
|
||||
}
|
||||
if (!is_numeric($this->default)) {
|
||||
throw new InvalidArgumentException('CommandArgument参数 ' . $name . ' 类型传入类型应为数字型,检测到非法的默认值 ' . $this->default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getTypeErrorPrompt(): string
|
||||
{
|
||||
return '参数类型错误,请重新输入!';
|
||||
}
|
||||
|
||||
public function getErrorQuitPrompt(): string
|
||||
{
|
||||
return '参数类型错误,停止输入!';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ZMKnownException
|
||||
*/
|
||||
protected function fixTypeName(string $type): string
|
||||
{
|
||||
$table = [
|
||||
'str' => 'string',
|
||||
'string' => 'string',
|
||||
'strings' => 'string',
|
||||
'byte' => 'string',
|
||||
'num' => 'number',
|
||||
'number' => 'number',
|
||||
'int' => 'number',
|
||||
'float' => 'number',
|
||||
'double' => 'number',
|
||||
'boolean' => 'bool',
|
||||
'bool' => 'bool',
|
||||
'true' => 'bool',
|
||||
'any' => 'any',
|
||||
'all' => 'any',
|
||||
'*' => 'any',
|
||||
];
|
||||
if (array_key_exists($type, $table)) {
|
||||
return $table[$type];
|
||||
}
|
||||
throw new ZMKnownException(zm_internal_errcode('E00077') . 'Invalid argument type: ' . $type . ', only support any, string, number and bool !');
|
||||
}
|
||||
}
|
||||
@ -28,33 +28,16 @@ class ServerStartCommand extends ServerCommand
|
||||
{
|
||||
$this->setAliases(['server:start']);
|
||||
$this->setDefinition([
|
||||
new InputOption('debug-mode', 'D', null, '开启调试模式 (这将关闭协程化)'),
|
||||
new InputOption('config-dir', null, InputOption::VALUE_REQUIRED, '指定其他配置文件目录'),
|
||||
new InputOption('driver', null, InputOption::VALUE_REQUIRED, '指定驱动类型'),
|
||||
new InputOption('log-debug', null, null, '调整消息等级到debug (log-level=4)'),
|
||||
new InputOption('log-level', null, InputOption::VALUE_REQUIRED, '调整消息等级到debug (log-level=4)'),
|
||||
new InputOption('log-verbose', null, null, '调整消息等级到verbose (log-level=3)'),
|
||||
new InputOption('log-info', null, null, '调整消息等级到info (log-level=2)'),
|
||||
new InputOption('log-warning', null, null, '调整消息等级到warning (log-level=1)'),
|
||||
new InputOption('log-error', null, null, '调整消息等级到error (log-level=0)'),
|
||||
new InputOption('log-theme', null, InputOption::VALUE_REQUIRED, '改变终端的主题配色'),
|
||||
new InputOption('disable-console-input', null, null, '禁止终端输入内容 (废弃)'),
|
||||
new InputOption('interact', null, null, '打开终端输入'),
|
||||
new InputOption('remote-terminal', null, null, '启用远程终端,配置使用global.php中的'),
|
||||
new InputOption('disable-coroutine', null, null, '关闭协程Hook'),
|
||||
new InputOption('daemon', null, null, '以守护进程的方式运行框架'),
|
||||
new InputOption('worker-num', null, InputOption::VALUE_REQUIRED, '启动框架时运行的 Worker 进程数量'),
|
||||
new InputOption('task-worker-num', null, InputOption::VALUE_REQUIRED, '启动框架时运行的 TaskWorker 进程数量'),
|
||||
new InputOption('watch', null, null, '监听 src/ 目录的文件变化并热更新'),
|
||||
new InputOption('show-php-ver', null, null, '启动时显示PHP和Swoole版本'),
|
||||
new InputOption('env', null, InputOption::VALUE_REQUIRED, '设置环境类型 (production, development, staging)'),
|
||||
new InputOption('disable-safe-exit', null, null, '关闭安全退出(关闭后按CtrlC时直接杀死进程)'),
|
||||
new InputOption('preview', null, null, '只显示参数,不启动服务器'),
|
||||
new InputOption('force-load-module', null, InputOption::VALUE_OPTIONAL, '强制打包状态下加载模块(使用英文逗号分割多个)'),
|
||||
new InputOption('polling-watch', null, null, '强制启用轮询模式监听'),
|
||||
new InputOption('no-state-check', null, null, '关闭启动前框架运行状态检查'),
|
||||
new InputOption('private-mode', null, null, '启动时隐藏MOTD和敏感信息'),
|
||||
new InputOption('audit-mode', null, null, '启动时开启审计模式,独立将所有日志输出到文件供开发人员审计'),
|
||||
]);
|
||||
$this->setDescription('Run zhamao-framework | 启动框架');
|
||||
$this->setHelp('直接运行可以启动');
|
||||
@ -84,7 +67,7 @@ class ServerStartCommand extends ServerCommand
|
||||
}
|
||||
}
|
||||
}
|
||||
(new Framework($input->getOptions()))->start();
|
||||
(new Framework($input->getOptions()))->init()->start();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,6 +108,13 @@ class ZMConfig
|
||||
self::$config_meta_list = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能patch,将patch数组内的数据合并更新到data中
|
||||
*
|
||||
* @param array|mixed $data 原数据
|
||||
* @param array|mixed $patch 要patch的数据
|
||||
* @return array|mixed
|
||||
*/
|
||||
public static function smartPatch($data, $patch)
|
||||
{
|
||||
/* patch 样例:
|
||||
@ -143,6 +150,8 @@ class ZMConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置文件
|
||||
*
|
||||
* @throws ConfigException
|
||||
* @return array|int|string
|
||||
*/
|
||||
@ -182,6 +191,7 @@ class ZMConfig
|
||||
|
||||
/**
|
||||
* 通过名称将所有该名称的配置文件路径和信息读取到列表中
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
private static function parseList(string $name): void
|
||||
@ -243,7 +253,7 @@ class ZMConfig
|
||||
if (!in_array($info['extension'], self::SUPPORTED_EXTENSIONS)) {
|
||||
continue;
|
||||
}
|
||||
if ($info['filename'] === $name) { // 如果文件名与配置文件名一致
|
||||
if ($info['filename'] === $name) { // 如果文件名与配置文件名一致,就创建一个配置文件的元数据对象
|
||||
$obj = new ConfigMetadata();
|
||||
$obj->is_patch = false;
|
||||
$obj->is_env = false;
|
||||
@ -258,11 +268,13 @@ class ZMConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $filename
|
||||
* @param mixed $ext_name
|
||||
* 根据不同的扩展类型读取配置文件数组
|
||||
*
|
||||
* @param mixed|string $filename 文件名
|
||||
* @param mixed|string $ext_name 扩展名
|
||||
* @throws ConfigException
|
||||
*/
|
||||
private static function readConfigFromFile($filename, $ext_name)
|
||||
private static function readConfigFromFile($filename, $ext_name): array
|
||||
{
|
||||
logger()->debug('正加载配置文件 ' . $filename);
|
||||
switch ($ext_name) {
|
||||
|
||||
@ -24,7 +24,7 @@ use ZM\Exception\InitException;
|
||||
*
|
||||
* 这里启动的不是框架,而是框架相关的命令行环境
|
||||
*/
|
||||
class ConsoleApplication extends Application
|
||||
final class ConsoleApplication extends Application
|
||||
{
|
||||
private static $obj;
|
||||
|
||||
@ -36,13 +36,6 @@ class ConsoleApplication extends Application
|
||||
if (self::$obj !== null) {
|
||||
throw new InitException(zm_internal_errcode('E00069') . 'Initializing another Application is not allowed!');
|
||||
}
|
||||
// 如果已经有定义了全局的 WORKING_DIR,那么就报错
|
||||
// if (defined('WORKING_DIR')) {
|
||||
// throw new InitException();
|
||||
// }
|
||||
|
||||
// 启动前检查炸毛运行情况
|
||||
// _zm_env_check();
|
||||
|
||||
// 初始化命令
|
||||
$this->add(new ServerStatusCommand()); // server运行状态
|
||||
|
||||
104
src/ZM/Container/BoundMethod.php
Normal file
104
src/ZM/Container/BoundMethod.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Container;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use ReflectionException;
|
||||
use ReflectionParameter;
|
||||
use ZM\Utils\ReflectionUtil;
|
||||
|
||||
class BoundMethod
|
||||
{
|
||||
/**
|
||||
* 调用指定闭包、类方法并注入依赖
|
||||
*
|
||||
* @param Container $container
|
||||
* @param callable|string $callback
|
||||
* @throws EntryResolutionException|ReflectionException
|
||||
* @throws InvalidArgumentException
|
||||
* @return mixed
|
||||
*/
|
||||
public static function call(ContainerInterface $container, $callback, array $parameters = [], string $default_method = null)
|
||||
{
|
||||
if (is_string($callback) && !$default_method && method_exists($callback, '__invoke')) {
|
||||
$default_method = '__invoke';
|
||||
}
|
||||
|
||||
if (is_string($callback) && $default_method) {
|
||||
$callback = [$callback, $default_method];
|
||||
}
|
||||
|
||||
if (ReflectionUtil::isNonStaticMethod($callback)) {
|
||||
$callback[0] = $container->make($callback[0]);
|
||||
}
|
||||
|
||||
if (!is_callable($callback)) {
|
||||
throw new InvalidArgumentException('Callback is not callable.');
|
||||
}
|
||||
|
||||
return call_user_func_array($callback, self::getMethodDependencies($container, $callback, $parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all dependencies for a given method.
|
||||
*
|
||||
* @param callable|string $callback
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
protected static function getMethodDependencies(ContainerInterface $container, $callback, array $parameters = []): array
|
||||
{
|
||||
$dependencies = [];
|
||||
|
||||
foreach (ReflectionUtil::getCallReflector($callback)->getParameters() as $i => $parameter) {
|
||||
if (isset($parameters[$i]) && $parameter->hasType() && ($type = $parameter->getType())) {
|
||||
if ($type instanceof \ReflectionNamedType && gettype($parameters[$i]) === $type->getName()) {
|
||||
$dependencies[] = $parameters[$i];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
|
||||
}
|
||||
|
||||
return array_merge($dependencies, array_values($parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dependency for the given call parameter.
|
||||
*
|
||||
* @throws EntryResolutionException
|
||||
*/
|
||||
protected static function addDependencyForCallParameter(
|
||||
ContainerInterface $container,
|
||||
ReflectionParameter $parameter,
|
||||
array &$parameters,
|
||||
array &$dependencies
|
||||
): void {
|
||||
if (array_key_exists($param_name = $parameter->getName(), $parameters)) {
|
||||
$dependencies[] = $parameters[$param_name];
|
||||
|
||||
unset($parameters[$param_name]);
|
||||
} elseif (!is_null($class_name = ReflectionUtil::getParameterClassName($parameter))) {
|
||||
if (array_key_exists($class_name, $parameters)) {
|
||||
$dependencies[] = $parameters[$class_name];
|
||||
|
||||
unset($parameters[$class_name]);
|
||||
} elseif ($parameter->isVariadic()) {
|
||||
$variadic_dependencies = $container->make($class_name);
|
||||
|
||||
$dependencies = array_merge($dependencies, is_array($variadic_dependencies)
|
||||
? $variadic_dependencies
|
||||
: [$variadic_dependencies]);
|
||||
} else {
|
||||
$dependencies[] = $container->make($class_name);
|
||||
}
|
||||
} elseif ($parameter->isDefaultValueAvailable()) {
|
||||
$dependencies[] = $parameter->getDefaultValue();
|
||||
} elseif (!array_key_exists($param_name, $parameters) && !$parameter->isOptional()) {
|
||||
$message = "无法解析类 {$parameter->getDeclaringClass()->getName()} 的依赖 {$parameter}";
|
||||
|
||||
throw new EntryResolutionException($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/ZM/Container/Container.php
Normal file
61
src/ZM/Container/Container.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Container;
|
||||
|
||||
use OneBot\Util\Singleton;
|
||||
|
||||
class Container implements ContainerInterface
|
||||
{
|
||||
use Singleton;
|
||||
use ContainerTrait {
|
||||
ContainerTrait::make as protected traitMake;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取父容器
|
||||
*/
|
||||
public function getParent(): ContainerInterface
|
||||
{
|
||||
return WorkerContainer::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the container can return an entry for the given identifier.
|
||||
* Returns false otherwise.
|
||||
*
|
||||
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
|
||||
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
|
||||
*
|
||||
* @param string $id identifier of the entry to look for
|
||||
*/
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return $this->bound($id) || $this->getParent()->has($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个绑定的实例
|
||||
*
|
||||
* @template T
|
||||
* @param class-string<T> $abstract 类或接口名
|
||||
* @param array $parameters 参数
|
||||
* @throws EntryResolutionException
|
||||
* @return Closure|mixed|T 实例
|
||||
*/
|
||||
public function make(string $abstract, array $parameters = [])
|
||||
{
|
||||
if (isset($this->shared[$abstract])) {
|
||||
return $this->shared[$abstract];
|
||||
}
|
||||
|
||||
// 此类没有,父类有,则从父类中获取
|
||||
if (!$this->bound($abstract) && $this->getParent()->bound($abstract)) {
|
||||
$this->log("{$abstract} is not bound, but in parent container, using parent container");
|
||||
return $this->getParent()->make($abstract, $parameters);
|
||||
}
|
||||
|
||||
return $this->traitMake($abstract, $parameters);
|
||||
}
|
||||
}
|
||||
110
src/ZM/Container/ContainerInterface.php
Normal file
110
src/ZM/Container/ContainerInterface.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Container;
|
||||
|
||||
use Closure;
|
||||
use Psr\Container\ContainerInterface as PsrContainerInterface;
|
||||
|
||||
/**
|
||||
* Interface ContainerInterface
|
||||
*
|
||||
* 从 Illuminate WorkerContainer 简化而来,兼容 PSR-11
|
||||
*/
|
||||
interface ContainerInterface extends PsrContainerInterface
|
||||
{
|
||||
/**
|
||||
* 判断对应的类或接口是否已经注册
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
*/
|
||||
public function bound(string $abstract): bool;
|
||||
|
||||
/**
|
||||
* 注册一个类别名
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param string $alias 别名
|
||||
*/
|
||||
public function alias(string $abstract, string $alias): void;
|
||||
|
||||
/**
|
||||
* 注册绑定
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
|
||||
* @param bool $shared 是否共享
|
||||
*/
|
||||
public function bind(string $abstract, $concrete = null, bool $shared = false): void;
|
||||
|
||||
/**
|
||||
* 注册绑定
|
||||
*
|
||||
* 在已经绑定时不会重复注册
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
|
||||
* @param bool $shared 是否共享
|
||||
*/
|
||||
public function bindIf(string $abstract, $concrete = null, bool $shared = false): void;
|
||||
|
||||
/**
|
||||
* 注册一个单例绑定
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
|
||||
*/
|
||||
public function singleton(string $abstract, $concrete = null): void;
|
||||
|
||||
/**
|
||||
* 注册一个单例绑定
|
||||
*
|
||||
* 在已经绑定时不会重复注册
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
|
||||
*/
|
||||
public function singletonIf(string $abstract, $concrete = null): void;
|
||||
|
||||
/**
|
||||
* 注册一个已有的实例,效果等同于单例绑定
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param mixed $instance 实例
|
||||
* @return mixed
|
||||
*/
|
||||
public function instance(string $abstract, $instance);
|
||||
|
||||
/**
|
||||
* 获取一个解析对应类实例的闭包
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
*/
|
||||
public function factory(string $abstract): Closure;
|
||||
|
||||
/**
|
||||
* 清除所有绑定和实例
|
||||
*/
|
||||
public function flush(): void;
|
||||
|
||||
/**
|
||||
* 获取一个绑定的实例
|
||||
*
|
||||
* @template T
|
||||
* @param class-string<T> $abstract 类或接口名
|
||||
* @param array $parameters 参数
|
||||
* @return Closure|mixed|T 实例
|
||||
*/
|
||||
public function make(string $abstract, array $parameters = []);
|
||||
|
||||
/**
|
||||
* 调用对应的方法,并自动注入依赖
|
||||
*
|
||||
* @param callable $callback 对应的方法
|
||||
* @param array $parameters 参数
|
||||
* @param null|string $default_method 默认方法
|
||||
* @return mixed
|
||||
*/
|
||||
public function call(callable $callback, array $parameters = [], string $default_method = null);
|
||||
}
|
||||
121
src/ZM/Container/ContainerServicesProvider.php
Normal file
121
src/ZM/Container/ContainerServicesProvider.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Container;
|
||||
|
||||
use Closure;
|
||||
use OneBot\Driver\Driver;
|
||||
use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||||
use OneBot\Driver\Process\ProcessManager;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Context\Context;
|
||||
use ZM\Context\ContextInterface;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Framework;
|
||||
|
||||
class ContainerServicesProvider
|
||||
{
|
||||
/**
|
||||
* 注册服务
|
||||
*
|
||||
* ```
|
||||
* 作用域:
|
||||
* global: worker start
|
||||
* request: request
|
||||
* message: message
|
||||
* connection: open, close, message
|
||||
* ```
|
||||
*
|
||||
* @param string $scope 作用域
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public function registerServices(string $scope, ...$params): void
|
||||
{
|
||||
switch ($scope) {
|
||||
case 'global':
|
||||
$this->registerGlobalServices(WorkerContainer::getInstance());
|
||||
break;
|
||||
case 'request':
|
||||
$this->registerRequestServices(Container::getInstance(), ...$params);
|
||||
break;
|
||||
case 'message':
|
||||
$this->registerConnectionServices(Container::getInstance());
|
||||
$this->registerMessageServices(Container::getInstance());
|
||||
break;
|
||||
case 'connection':
|
||||
$this->registerConnectionServices(Container::getInstance());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理服务
|
||||
*/
|
||||
public function cleanup(): void
|
||||
{
|
||||
container()->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局服务
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
private function registerGlobalServices(ContainerInterface $container): void
|
||||
{
|
||||
// 注册路径类的容器快捷方式
|
||||
$container->instance('path.working', WORKING_DIR);
|
||||
$container->instance('path.source', SOURCE_ROOT_DIR);
|
||||
$container->alias('path.source', 'path.base');
|
||||
$container->instance('path.data', ZMConfig::get('global.data_dir'));
|
||||
$container->instance('path.framework', FRAMEWORK_ROOT_DIR);
|
||||
|
||||
// 注册worker和驱动
|
||||
$container->instance('worker_id', ProcessManager::getProcessId());
|
||||
$container->instance(Driver::class, Framework::getInstance()->getDriver());
|
||||
|
||||
// 注册logger
|
||||
$container->instance(LoggerInterface::class, logger());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册请求服务(HTTP请求)
|
||||
*/
|
||||
private function registerRequestServices(ContainerInterface $container, HttpRequestEvent $event): void
|
||||
{
|
||||
// $context = Context::$context[zm_cid()];
|
||||
$container->instance(HttpRequestEvent::class, $event);
|
||||
$container->alias('http.request.event', HttpRequestEvent::class);
|
||||
$container->instance(ServerRequestInterface::class, $event->getRequest());
|
||||
$container->alias('http.request', ServerRequestInterface::class);
|
||||
// $container->instance(Request::class, $context['request']);
|
||||
// $container->instance(Response::class, $context['response']);
|
||||
$container->bind(ContextInterface::class, Context::class);
|
||||
// $container->alias(ContextInterface::class, Context::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册消息服务(WS消息)
|
||||
*/
|
||||
private function registerMessageServices(ContainerInterface $container): void
|
||||
{
|
||||
// $context = Context::$context[zm_cid()];
|
||||
// $container->instance(Frame::class, $context['frame']); // WS 消息帧
|
||||
// $container->bind(ContextInterface::class, Closure::fromCallable('ctx'));
|
||||
// $container->alias(ContextInterface::class, Context::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册链接服务
|
||||
*/
|
||||
private function registerConnectionServices(ContainerInterface $container): void
|
||||
{
|
||||
// $context = Context::$context[zm_cid()];
|
||||
// $container->instance(ConnectionObject::class, $context['connection']);
|
||||
}
|
||||
}
|
||||
735
src/ZM/Container/ContainerTrait.php
Normal file
735
src/ZM/Container/ContainerTrait.php
Normal file
@ -0,0 +1,735 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Container;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionParameter;
|
||||
use ZM\Utils\ReflectionUtil;
|
||||
|
||||
trait ContainerTrait
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $shared = [];
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
*/
|
||||
protected $build_stack = [];
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
*/
|
||||
protected $with = [];
|
||||
|
||||
/**
|
||||
* 日志前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $log_prefix;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
*/
|
||||
private static $bindings = [];
|
||||
|
||||
/**
|
||||
* @var object[]
|
||||
*/
|
||||
private static $instances = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private static $aliases = [];
|
||||
|
||||
/**
|
||||
* @var Closure[][]
|
||||
*/
|
||||
private static $extenders = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if ($this->shouldLog()) {
|
||||
$this->log('Container created');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对应的类或接口是否已经注册
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
*/
|
||||
public function bound(string $abstract): bool
|
||||
{
|
||||
return array_key_exists($abstract, self::$bindings)
|
||||
|| array_key_exists($abstract, self::$instances)
|
||||
|| array_key_exists($abstract, $this->shared)
|
||||
|| $this->isAlias($abstract);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类别名(如存在)
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @return string 别名,不存在时返回传入的类或接口名
|
||||
*/
|
||||
public function getAlias(string $abstract): string
|
||||
{
|
||||
if (!isset(self::$aliases[$abstract])) {
|
||||
return $abstract;
|
||||
}
|
||||
|
||||
return $this->getAlias(self::$aliases[$abstract]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个类别名
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param string $alias 别名
|
||||
*/
|
||||
public function alias(string $abstract, string $alias): void
|
||||
{
|
||||
if ($alias === $abstract) {
|
||||
throw new InvalidArgumentException("[{$abstract}] is same as [{$alias}]");
|
||||
}
|
||||
|
||||
self::$aliases[$alias] = $abstract;
|
||||
|
||||
if ($this->shouldLog()) {
|
||||
$this->log("[{$abstract}] is aliased as [{$alias}]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册绑定
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
|
||||
* @param bool $shared 是否共享
|
||||
*/
|
||||
public function bind(string $abstract, $concrete = null, bool $shared = false): void
|
||||
{
|
||||
$this->dropStaleInstances($abstract);
|
||||
|
||||
// 如果没有提供闭包,则默认为自动解析类名
|
||||
if (is_null($concrete)) {
|
||||
$concrete = $abstract;
|
||||
}
|
||||
|
||||
$concrete_name = '';
|
||||
if ($this->shouldLog()) {
|
||||
$concrete_name = ReflectionUtil::variableToString($concrete);
|
||||
}
|
||||
|
||||
// 如果不是闭包,则认为是类名,此时将其包装在一个闭包中,以方便后续处理
|
||||
if (!$concrete instanceof Closure) {
|
||||
$concrete = $this->getClosure($abstract, $concrete);
|
||||
}
|
||||
|
||||
self::$bindings[$abstract] = compact('concrete', 'shared');
|
||||
|
||||
if ($this->shouldLog()) {
|
||||
$this->log("[{$abstract}] is bound to [{$concrete_name}]" . ($shared ? ' (shared)' : ''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册绑定
|
||||
*
|
||||
* 在已经绑定时不会重复注册
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
|
||||
* @param bool $shared 是否共享
|
||||
*/
|
||||
public function bindIf(string $abstract, $concrete = null, bool $shared = false): void
|
||||
{
|
||||
if (!$this->bound($abstract)) {
|
||||
$this->bind($abstract, $concrete, $shared);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个单例绑定
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
|
||||
*/
|
||||
public function singleton(string $abstract, $concrete = null): void
|
||||
{
|
||||
$this->bind($abstract, $concrete, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个单例绑定
|
||||
*
|
||||
* 在已经绑定时不会重复注册
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
|
||||
*/
|
||||
public function singletonIf(string $abstract, $concrete = null): void
|
||||
{
|
||||
if (!$this->bound($abstract)) {
|
||||
$this->singleton($abstract, $concrete);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个已有的实例,效果等同于单例绑定
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param mixed $instance 实例
|
||||
* @return mixed
|
||||
*/
|
||||
public function instance(string $abstract, $instance)
|
||||
{
|
||||
if (isset(self::$instances[$abstract])) {
|
||||
return self::$instances[$abstract];
|
||||
}
|
||||
|
||||
self::$instances[$abstract] = $instance;
|
||||
|
||||
if ($this->shouldLog()) {
|
||||
$class_name = ReflectionUtil::variableToString($instance);
|
||||
$this->log("[{$abstract}] is bound to [{$class_name}] (instance)");
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个解析对应类实例的闭包
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
*/
|
||||
public function factory(string $abstract): Closure
|
||||
{
|
||||
return function () use ($abstract) {
|
||||
return $this->make($abstract);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有绑定和实例
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
self::$aliases = [];
|
||||
self::$bindings = [];
|
||||
self::$instances = [];
|
||||
|
||||
$this->shared = [];
|
||||
$this->build_stack = [];
|
||||
$this->with = [];
|
||||
|
||||
if ($this->shouldLog()) {
|
||||
$this->log('Container flushed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个绑定的实例
|
||||
*
|
||||
* @template T
|
||||
* @param class-string<T> $abstract 类或接口名
|
||||
* @param array $parameters 参数
|
||||
* @throws EntryResolutionException
|
||||
* @return Closure|mixed|T 实例
|
||||
*/
|
||||
public function make(string $abstract, array $parameters = [])
|
||||
{
|
||||
$abstract = $this->getAlias($abstract);
|
||||
|
||||
$needs_contextual_build = !empty($parameters);
|
||||
|
||||
if (isset($this->shared[$abstract])) {
|
||||
if ($this->shouldLog()) {
|
||||
$this->log(sprintf(
|
||||
'[%s] resolved (shared)%s',
|
||||
$abstract,
|
||||
$needs_contextual_build ? ' with ' . implode(', ', $parameters) : ''
|
||||
));
|
||||
}
|
||||
return $this->shared[$abstract];
|
||||
}
|
||||
|
||||
// 如果已经存在在实例池中(通常意味着单例绑定),则直接返回该实例
|
||||
if (isset(self::$instances[$abstract]) && !$needs_contextual_build) {
|
||||
if ($this->shouldLog()) {
|
||||
$this->log("[{$abstract}] resolved (instance)");
|
||||
}
|
||||
return self::$instances[$abstract];
|
||||
}
|
||||
|
||||
$this->with[] = $parameters;
|
||||
|
||||
$concrete = $this->getConcrete($abstract);
|
||||
|
||||
// 构造该类的实例,并递归解析所有依赖
|
||||
if ($this->isBuildable($concrete, $abstract)) {
|
||||
$object = $this->build($concrete);
|
||||
} else {
|
||||
$object = $this->make($concrete);
|
||||
}
|
||||
|
||||
// 如果该类存在扩展器(装饰器),则逐个应用到实例
|
||||
foreach ($this->getExtenders($abstract) as $extender) {
|
||||
$object = $extender($object, $this);
|
||||
}
|
||||
|
||||
// 如果该类被注册为单例,则需要将其存放在实例池中,方便后续取用同一实例
|
||||
if (!$needs_contextual_build && $this->isShared($abstract)) {
|
||||
$this->shared[$abstract] = $object;
|
||||
if ($this->shouldLog()) {
|
||||
$this->log("[{$abstract}] added to shared pool");
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出本次构造的覆盖参数
|
||||
array_pop($this->with);
|
||||
|
||||
if ($this->shouldLog()) {
|
||||
$this->log(sprintf(
|
||||
'[%s] resolved%s',
|
||||
$abstract,
|
||||
$needs_contextual_build ? ' with ' . implode(', ', $parameters) : ''
|
||||
));
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例化具体的类实例
|
||||
*
|
||||
* @param Closure|string $concrete 类名或对应的闭包
|
||||
* @throws EntryResolutionException
|
||||
* @return mixed
|
||||
*/
|
||||
public function build($concrete)
|
||||
{
|
||||
// 如果传入的是闭包,则直接执行并返回
|
||||
if ($concrete instanceof Closure) {
|
||||
return $concrete($this, $this->getLastParameterOverride());
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new ReflectionClass($concrete);
|
||||
} catch (ReflectionException $e) {
|
||||
throw new EntryResolutionException("指定的类 {$concrete} 不存在", 0, $e);
|
||||
}
|
||||
|
||||
if (!$reflection->isInstantiable()) {
|
||||
$this->notInstantiable($concrete);
|
||||
}
|
||||
|
||||
$this->build_stack[] = $concrete;
|
||||
|
||||
$constructor = $reflection->getConstructor();
|
||||
|
||||
// 如果不存在构造函数,则代表不需要进一步解析,直接实例化即可
|
||||
if (is_null($constructor)) {
|
||||
array_pop($this->build_stack);
|
||||
return new $concrete();
|
||||
}
|
||||
|
||||
$dependencies = $constructor->getParameters();
|
||||
|
||||
// 获取所有依赖的实例
|
||||
try {
|
||||
$instances = $this->resolveDependencies($dependencies);
|
||||
} catch (EntryResolutionException $e) {
|
||||
array_pop($this->build_stack);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
array_pop($this->build_stack);
|
||||
|
||||
return $reflection->newInstanceArgs($instances);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用对应的方法,并自动注入依赖
|
||||
*
|
||||
* @param callable|string $callback 对应的方法
|
||||
* @param array $parameters 参数
|
||||
* @param null|string $default_method 默认方法
|
||||
* @return mixed
|
||||
*/
|
||||
public function call($callback, array $parameters = [], string $default_method = null)
|
||||
{
|
||||
if ($this->shouldLog()) {
|
||||
if (count($parameters)) {
|
||||
$str_parameters = array_map([ReflectionUtil::class, 'variableToString'], $parameters);
|
||||
$str_parameters = implode(', ', $str_parameters);
|
||||
} else {
|
||||
$str_parameters = '';
|
||||
}
|
||||
$this->log(sprintf(
|
||||
'Called %s%s(%s)',
|
||||
ReflectionUtil::variableToString($callback),
|
||||
$default_method ? '@' . $default_method : '',
|
||||
$str_parameters
|
||||
));
|
||||
}
|
||||
return BoundMethod::call($this, $callback, $parameters, $default_method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an entry of the container by its identifier and returns it.
|
||||
*
|
||||
* @param string $id identifier of the entry to look for
|
||||
*
|
||||
* @throws NotFoundExceptionInterface no entry was found for **this** identifier
|
||||
* @throws ContainerExceptionInterface error while retrieving the entry
|
||||
*
|
||||
* @return mixed entry
|
||||
*/
|
||||
public function get(string $id)
|
||||
{
|
||||
try {
|
||||
return $this->make($id);
|
||||
} catch (Exception $e) {
|
||||
if ($this->has($id)) {
|
||||
throw new EntryResolutionException('', 0, $e);
|
||||
}
|
||||
|
||||
throw new EntryNotFoundException('', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the container can return an entry for the given identifier.
|
||||
* Returns false otherwise.
|
||||
*
|
||||
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
|
||||
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
|
||||
*
|
||||
* @param string $id identifier of the entry to look for
|
||||
*/
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return $this->bound($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展一个类或接口
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param Closure $closure 扩展闭包
|
||||
*/
|
||||
public function extend(string $abstract, Closure $closure): void
|
||||
{
|
||||
$abstract = $this->getAlias($abstract);
|
||||
|
||||
// 如果该类已经被解析过,则直接将扩展器应用到该类的实例上
|
||||
// 否则,将扩展器存入扩展器池,等待解析
|
||||
if (isset(self::$instances[$abstract])) {
|
||||
self::$instances[$abstract] = $closure(self::$instances[$abstract], $this);
|
||||
} else {
|
||||
self::$extenders[$abstract][] = $closure;
|
||||
}
|
||||
|
||||
if ($this->shouldLog()) {
|
||||
$this->log("[{$abstract}] extended");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志前缀
|
||||
*/
|
||||
public function getLogPrefix(): string
|
||||
{
|
||||
return ($this->log_prefix ?: '[WorkerContainer(U)]') . ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志前缀
|
||||
*/
|
||||
public function setLogPrefix(string $prefix): void
|
||||
{
|
||||
$this->log_prefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应类型的所有扩展器
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @return Closure[]
|
||||
*/
|
||||
protected function getExtenders(string $abstract): array
|
||||
{
|
||||
$abstract = $this->getAlias($abstract);
|
||||
|
||||
return self::$extenders[$abstract] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断传入的是否为别名
|
||||
*/
|
||||
protected function isAlias(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, self::$aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛弃所有过时的实例和别名
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
*/
|
||||
protected function dropStaleInstances(string $abstract): void
|
||||
{
|
||||
unset(
|
||||
self::$instances[$abstract],
|
||||
self::$aliases[$abstract],
|
||||
$this->shared[$abstract]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个解析对应类的闭包
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @param string $concrete 实际类名
|
||||
*/
|
||||
protected function getClosure(string $abstract, string $concrete): Closure
|
||||
{
|
||||
return static function ($container, $parameters = []) use ($abstract, $concrete) {
|
||||
$method = $abstract === $concrete ? 'build' : 'make';
|
||||
|
||||
return $container->{$method}($concrete, $parameters);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后一次的覆盖参数
|
||||
*/
|
||||
protected function getLastParameterOverride(): array
|
||||
{
|
||||
return $this->with[count($this->with) - 1] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出实例化异常
|
||||
*
|
||||
* @throws EntryResolutionException
|
||||
*/
|
||||
protected function notInstantiable(string $concrete, string $reason = ''): void
|
||||
{
|
||||
if (!empty($this->build_stack)) {
|
||||
$previous = implode(', ', $this->build_stack);
|
||||
$message = "类 {$concrete} 无法实例化,其被 {$previous} 依赖";
|
||||
} else {
|
||||
$message = "类 {$concrete} 无法实例化";
|
||||
}
|
||||
|
||||
throw new EntryResolutionException("{$message}:{$reason}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析依赖
|
||||
*
|
||||
* @param ReflectionParameter[] $dependencies
|
||||
* @throws EntryResolutionException
|
||||
*/
|
||||
protected function resolveDependencies(array $dependencies): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($dependencies as $dependency) {
|
||||
// 如果此依赖存在覆盖参数,则使用覆盖参数
|
||||
// 否则,将尝试解析参数
|
||||
if ($this->hasParameterOverride($dependency)) {
|
||||
$results[] = $this->getParameterOverride($dependency);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果存在临时注入的依赖,则使用临时注入的依赖
|
||||
if ($this->hasParameterTypeOverride($dependency)) {
|
||||
$results[] = $this->getParameterTypeOverride($dependency);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果类名为空,则代表此依赖是基本类型,且无法对其进行依赖解析
|
||||
$class_name = ReflectionUtil::getParameterClassName($dependency);
|
||||
$results[] = is_null($class_name)
|
||||
? $this->resolvePrimitive($dependency)
|
||||
: $this->resolveClass($dependency);
|
||||
|
||||
if ($this->shouldLog()) {
|
||||
if (is_null($class_name)) {
|
||||
if ($dependency->hasType()) {
|
||||
$class_name = $dependency->getType();
|
||||
} else {
|
||||
$class_name = 'Primitive';
|
||||
}
|
||||
}
|
||||
$this->log("Dependency [{$class_name} {$dependency->name}] resolved");
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断传入的参数是否存在覆盖参数
|
||||
*/
|
||||
protected function hasParameterOverride(ReflectionParameter $parameter): bool
|
||||
{
|
||||
return array_key_exists($parameter->name, $this->getLastParameterOverride());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取覆盖参数
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getParameterOverride(ReflectionParameter $parameter)
|
||||
{
|
||||
return $this->getLastParameterOverride()[$parameter->name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断传入的参数是否存在临时注入的参数
|
||||
*/
|
||||
protected function hasParameterTypeOverride(ReflectionParameter $parameter): bool
|
||||
{
|
||||
if (!$parameter->hasType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $parameter->getType();
|
||||
|
||||
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_key_exists($type->getName(), $this->getLastParameterOverride());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时注入的参数
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getParameterTypeOverride(ReflectionParameter $parameter)
|
||||
{
|
||||
$type = $parameter->getType();
|
||||
|
||||
if (!$type instanceof ReflectionNamedType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->getLastParameterOverride()[$type->getName()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析基本类型
|
||||
*
|
||||
* @throws EntryResolutionException 如参数不存在默认值,则抛出异常
|
||||
* @return mixed 对应类型的默认值
|
||||
*/
|
||||
protected function resolvePrimitive(ReflectionParameter $parameter)
|
||||
{
|
||||
if ($parameter->isDefaultValueAvailable()) {
|
||||
return $parameter->getDefaultValue();
|
||||
}
|
||||
|
||||
throw new EntryResolutionException("无法解析类 {$parameter->getDeclaringClass()->getName()} 的参数 {$parameter}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析类
|
||||
*
|
||||
* @throws EntryResolutionException 如果无法解析类,则抛出异常
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveClass(ReflectionParameter $parameter)
|
||||
{
|
||||
try {
|
||||
// 尝试解析
|
||||
return $this->make(ReflectionUtil::getParameterClassName($parameter));
|
||||
} catch (EntryResolutionException $e) {
|
||||
// 如果参数是可选的,则返回默认值
|
||||
if ($parameter->isDefaultValueAvailable()) {
|
||||
array_pop($this->with);
|
||||
return $parameter->getDefaultValue();
|
||||
}
|
||||
|
||||
if ($parameter->isVariadic()) {
|
||||
array_pop($this->with);
|
||||
return [];
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类名的实际类型
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
* @return Closure|string
|
||||
*/
|
||||
protected function getConcrete(string $abstract)
|
||||
{
|
||||
if (isset(self::$bindings[$abstract])) {
|
||||
return self::$bindings[$abstract]['concrete'];
|
||||
}
|
||||
|
||||
return $abstract;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断传入的实际类型是否可以构造
|
||||
*
|
||||
* @param mixed $concrete 实际类型
|
||||
* @param string $abstract 类或接口名
|
||||
*/
|
||||
protected function isBuildable($concrete, string $abstract): bool
|
||||
{
|
||||
return $concrete === $abstract || $concrete instanceof Closure;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断传入的类型是否为共享实例
|
||||
*
|
||||
* @param string $abstract 类或接口名
|
||||
*/
|
||||
protected function isShared(string $abstract): bool
|
||||
{
|
||||
return isset($this->instances[$abstract])
|
||||
|| (isset($this->bindings[$abstract]['shared'])
|
||||
&& $this->bindings[$abstract]['shared'] === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否输出日志
|
||||
*/
|
||||
protected function shouldLog(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志(自动附加容器日志前缀)
|
||||
*/
|
||||
protected function log(string $message): void
|
||||
{
|
||||
logger()->debug($this->getLogPrefix() . $message);
|
||||
}
|
||||
}
|
||||
12
src/ZM/Container/EntryNotFoundException.php
Normal file
12
src/ZM/Container/EntryNotFoundException.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Container;
|
||||
|
||||
use Exception;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
class EntryNotFoundException extends Exception implements NotFoundExceptionInterface
|
||||
{
|
||||
}
|
||||
12
src/ZM/Container/EntryResolutionException.php
Normal file
12
src/ZM/Container/EntryResolutionException.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Container;
|
||||
|
||||
use Exception;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
|
||||
class EntryResolutionException extends Exception implements ContainerExceptionInterface
|
||||
{
|
||||
}
|
||||
13
src/ZM/Container/WorkerContainer.php
Normal file
13
src/ZM/Container/WorkerContainer.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Container;
|
||||
|
||||
use OneBot\Util\Singleton;
|
||||
|
||||
class WorkerContainer implements ContainerInterface
|
||||
{
|
||||
use Singleton;
|
||||
use ContainerTrait;
|
||||
}
|
||||
23
src/ZM/Context/Context.php
Normal file
23
src/ZM/Context/Context.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Context;
|
||||
|
||||
use ZM\Context\Trait\HttpTrait;
|
||||
|
||||
/**
|
||||
* 下面是机器人类的方法
|
||||
* @method reply($message) 快速回复消息
|
||||
* @method action(string $action, array $params = []) 执行动作
|
||||
* @method getArgument(string $name) 获取BotCommand的参数
|
||||
* @method getRawArguments() 获取裸的参数
|
||||
* @method getBotEvent(bool $array = false) 获取事件原对象
|
||||
* @method getBotSelf() 获取机器人自身的信息
|
||||
*/
|
||||
class Context implements ContextInterface
|
||||
{
|
||||
use HttpTrait;
|
||||
|
||||
// TODO:完善上下文的方法们
|
||||
}
|
||||
30
src/ZM/Context/ContextInterface.php
Normal file
30
src/ZM/Context/ContextInterface.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Context;
|
||||
|
||||
use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
interface ContextInterface
|
||||
{
|
||||
/**
|
||||
* 获取 Http Request 请求对象
|
||||
*/
|
||||
public function getRequest(): ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* 获取 Http 请求事件对象
|
||||
*/
|
||||
public function getHttpRequestEvent(): HttpRequestEvent;
|
||||
|
||||
/**
|
||||
* 使用 Response 对象响应 Http 请求
|
||||
* Wrapper of HttpRequestEvent::withResponse method
|
||||
*
|
||||
* @param ResponseInterface $response 响应对象
|
||||
*/
|
||||
public function withResponse(ResponseInterface $response);
|
||||
}
|
||||
41
src/ZM/Context/Trait/HttpTrait.php
Normal file
41
src/ZM/Context/Trait/HttpTrait.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Context\Trait;
|
||||
|
||||
use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use ZM\Exception\ZMKnownException;
|
||||
|
||||
trait HttpTrait
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return container()->get('http.request');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getHttpRequestEvent(): HttpRequestEvent
|
||||
{
|
||||
$obj = container()->get('http.request.event');
|
||||
if (!$obj instanceof HttpRequestEvent) {
|
||||
throw new ZMKnownException('E00099', 'current context container event is not HttpRequestEvent');
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withResponse(ResponseInterface $response)
|
||||
{
|
||||
$this->getHttpRequestEvent()->withResponse($response);
|
||||
}
|
||||
}
|
||||
@ -5,21 +5,81 @@ declare(strict_types=1);
|
||||
namespace ZM\Event\Listener;
|
||||
|
||||
use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||||
use OneBot\Driver\Event\StopException;
|
||||
use OneBot\Http\HttpFactory;
|
||||
use OneBot\Http\Stream;
|
||||
use OneBot\Util\Singleton;
|
||||
use Stringable;
|
||||
use Throwable;
|
||||
use ZM\Annotation\AnnotationHandler;
|
||||
use ZM\Annotation\Framework\BindEvent;
|
||||
use ZM\Annotation\Http\Route;
|
||||
use ZM\Container\ContainerServicesProvider;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Utils\HttpUtil;
|
||||
|
||||
class HttpEventListener
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* @throws StopException
|
||||
* 框架自身要实现的 HttpRequestEvent 事件回调
|
||||
* 这里处理框架特有的内容,比如:
|
||||
* 路由、断点续传、注解再分发等
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function onRequest(HttpRequestEvent $event)
|
||||
public function onRequest999(HttpRequestEvent $event)
|
||||
{
|
||||
$msg = 'Hello from ' . $event->getSocketFlag();
|
||||
$res = HttpFactory::getInstance()->createResponse()->withBody(HttpFactory::getInstance()->createStream($msg));
|
||||
$event->withResponse($res);
|
||||
// 注册容器
|
||||
resolve(ContainerServicesProvider::class)->registerServices('request', $event);
|
||||
// 跑一遍 BindEvent 绑定了 HttpRequestEvent 的注解
|
||||
$handler = new AnnotationHandler(BindEvent::class);
|
||||
$handler->setRuleCallback(function (BindEvent $anno) {
|
||||
return $anno->event_class === HttpRequestEvent::class;
|
||||
});
|
||||
$handler->handleAll($event);
|
||||
// dump($event->getResponse());
|
||||
$node = null;
|
||||
$params = null;
|
||||
// 如果状态是 Normal,那么说明跑了一遍没有阻塞或者其他的情况,我就直接跑一遍内部的路由分发和静态文件分发
|
||||
if ($handler->getStatus() === AnnotationHandler::STATUS_NORMAL && $event->getResponse() === null) {
|
||||
// 解析路由和路由状态
|
||||
$result = HttpUtil::parseUri($event->getRequest(), $node, $params);
|
||||
switch ($result) {
|
||||
case ZM_ERR_NONE: // 解析到存在路由了
|
||||
$handler = new AnnotationHandler(Route::class);
|
||||
$div = new Route($node['route']);
|
||||
$div->params = $params;
|
||||
$div->method = $node['method'];
|
||||
$div->request_method = $node['request_method'];
|
||||
$div->class = $node['class'];
|
||||
$starttime = microtime(true);
|
||||
$handler->handle($div, null, $params, $event->getRequest(), $event);
|
||||
if (is_string($val = $handler->getReturnVal()) || ($val instanceof Stringable)) {
|
||||
$event->withResponse(HttpFactory::getInstance()->createResponse(200, null, [], Stream::create($val)));
|
||||
} elseif ($event->getResponse() === null) {
|
||||
$event->withResponse(HttpFactory::getInstance()->createResponse(500));
|
||||
}
|
||||
logger()->warning('Used ' . round((microtime(true) - $starttime) * 1000, 3) . ' ms');
|
||||
break;
|
||||
case ZM_ERR_ROUTE_METHOD_NOT_ALLOWED:
|
||||
$event->withResponse(HttpUtil::handleHttpCodePage(405));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历结束所有的如果还是没有响应,那么就找静态文件路由
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public function onRequest1(HttpRequestEvent $event)
|
||||
{
|
||||
if ($event->getResponse() === null) {
|
||||
$response = HttpUtil::handleStaticPage($event->getRequest()->getUri()->getPath());
|
||||
$event->withResponse($response);
|
||||
}
|
||||
container()->flush();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,25 +5,36 @@ declare(strict_types=1);
|
||||
namespace ZM\Event\Listener;
|
||||
|
||||
use OneBot\Util\Singleton;
|
||||
use ZM\Exception\ZMKnownException;
|
||||
use ZM\Process\ProcessStateManager;
|
||||
|
||||
class ManagerEventListener
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* Manager 进程启动的回调(仅 Swoole 驱动才会回调)
|
||||
*/
|
||||
public function onManagerStart()
|
||||
{
|
||||
// 自注册一下,刷新当前进程的logger进程banner
|
||||
ob_logger_register(ob_logger());
|
||||
logger()->debug('Manager process started');
|
||||
|
||||
// 注册 Manager 进程的信号
|
||||
SignalListener::getInstance()->signalManager();
|
||||
|
||||
/* @noinspection PhpComposerExtensionStubsInspection */
|
||||
ProcessStateManager::saveProcessState(ZM_PROCESS_MANAGER, posix_getpid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager 进程停止的回调(仅 Swoole 驱动才会回调)
|
||||
* @throws ZMKnownException
|
||||
*/
|
||||
public function onManagerStop()
|
||||
{
|
||||
logger()->debug('Manager process stopped');
|
||||
ProcessStateManager::removeProcessState(ZM_PROCESS_MANAGER);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,23 +6,38 @@ namespace ZM\Event\Listener;
|
||||
|
||||
use OneBot\Driver\Process\ProcessManager;
|
||||
use OneBot\Util\Singleton;
|
||||
use Throwable;
|
||||
use ZM\Annotation\AnnotationHandler;
|
||||
use ZM\Annotation\AnnotationMap;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Framework\Init;
|
||||
use ZM\Container\ContainerServicesProvider;
|
||||
use ZM\Exception\ZMKnownException;
|
||||
use ZM\Framework;
|
||||
use ZM\Process\ProcessStateManager;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
class WorkerEventListener
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* Driver 的 Worker 进程启动后执行的事件
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function onWorkerStart()
|
||||
{
|
||||
// 自注册一下,刷新当前进程的logger进程banner
|
||||
ob_logger_register(ob_logger());
|
||||
|
||||
// 如果没有引入参数disable-safe-exit,则监听 Ctrl+C
|
||||
if (!Framework::getInstance()->getArgv()['disable-safe-exit'] && PHP_OS_FAMILY !== 'Windows') {
|
||||
SignalListener::getInstance()->signalWorker();
|
||||
}
|
||||
logger()->debug('Worker #' . ProcessManager::getProcessId() . ' started');
|
||||
|
||||
// 设置 Worker 进程的状态和 ID 等信息
|
||||
if (($name = Framework::getInstance()->getDriver()->getName()) === 'swoole') {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$server = Framework::getInstance()->getDriver()->getSwooleServer();
|
||||
@ -30,11 +45,81 @@ class WorkerEventListener
|
||||
} elseif ($name === 'workerman' && DIRECTORY_SEPARATOR !== '\\' && extension_loaded('posix')) {
|
||||
ProcessStateManager::saveProcessState(ZM_PROCESS_WORKER, posix_getpid(), ['worker_id' => ProcessManager::getProcessId()]);
|
||||
}
|
||||
|
||||
// 设置容器,注册容器提供商
|
||||
resolve(ContainerServicesProvider::class)->registerServices('global');
|
||||
|
||||
// 注册 Worker 进程遇到退出时的回调,安全退出
|
||||
register_shutdown_function(function () {
|
||||
$error = error_get_last();
|
||||
// 下面这段代码的作用就是,不是错误引发的退出时照常退出即可
|
||||
if (($error['type'] ?? 0) != 0) {
|
||||
logger()->emergency(zm_internal_errcode('E00027') . 'Internal fatal error: ' . $error['message'] . ' at ' . $error['file'] . "({$error['line']})");
|
||||
} elseif (!isset($error['type'])) {
|
||||
return;
|
||||
}
|
||||
Framework::getInstance()->stop();
|
||||
});
|
||||
|
||||
// TODO: 注册各种池子
|
||||
|
||||
// 加载用户代码资源
|
||||
$this->loadUserSources();
|
||||
|
||||
// handle @Init annotation
|
||||
$this->handleInit();
|
||||
|
||||
// 回显 debug 日志:进程占用的内存
|
||||
$memory_total = memory_get_usage() / 1024 / 1024;
|
||||
logger()->debug('Worker process used ' . round($memory_total, 3) . ' MB');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ZMKnownException
|
||||
*/
|
||||
public function onWorkerStop()
|
||||
{
|
||||
logger()->debug('Worker #' . ProcessManager::getProcessId() . ' stopping');
|
||||
ProcessStateManager::removeProcessState(ZM_PROCESS_WORKER, ProcessManager::getProcessId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户代码资源,包括普通插件、单文件插件、Composer 插件等
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function loadUserSources()
|
||||
{
|
||||
logger()->debug('Loading user sources');
|
||||
|
||||
// 首先先加载 source 普通插件,相当于内部模块,不算插件的一种
|
||||
$parser = new AnnotationParser();
|
||||
$composer = ZMUtil::getComposerMetadata();
|
||||
// 合并 dev 和 非 dev 的 psr-4 加载目录
|
||||
$merge_psr4 = array_merge($composer['autoload']['psr-4'] ?? [], $composer['autoload-dev']['psr-4'] ?? []);
|
||||
// 排除 composer.json 中指定需要排除的目录
|
||||
$excludes = $composer['extra']['zm']['exclude-annotation-path'] ?? [];
|
||||
foreach ($merge_psr4 as $k => $v) {
|
||||
// 如果在排除表就排除,否则就解析注解
|
||||
if (is_dir(SOURCE_ROOT_DIR . '/' . $v) && !in_array($v, $excludes)) {
|
||||
// 添加解析路径,对应Base命名空间也贴出来
|
||||
$parser->addRegisterPath(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\'));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 然后加载插件目录下的插件
|
||||
|
||||
// 解析所有注册路径的文件,获取注解
|
||||
$parser->parseAll();
|
||||
// 将Parser解析后的注解注册到全局的 AnnotationMap
|
||||
AnnotationMap::loadAnnotationByParser($parser);
|
||||
}
|
||||
|
||||
private function handleInit()
|
||||
{
|
||||
$handler = new AnnotationHandler(Init::class);
|
||||
$handler->setRuleCallback(function (Init $anno) {
|
||||
return $anno->worker === -1 || $anno->worker === ProcessManager::getProcessId();
|
||||
});
|
||||
$handler->handleAll();
|
||||
}
|
||||
}
|
||||
|
||||
15
src/ZM/Exception/InvalidArgumentException.php
Normal file
15
src/ZM/Exception/InvalidArgumentException.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class InvalidArgumentException extends ZMException
|
||||
{
|
||||
public function __construct($message = '', $code = 0, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(zm_internal_errcode('E00074') . $message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,6 @@ class Framework
|
||||
*
|
||||
* @param array<string, null|bool|string> $argv 传入的参数(见 ServerStartCommand)
|
||||
* @throws InitException
|
||||
* @throws ConfigException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(array $argv = [])
|
||||
@ -73,7 +72,13 @@ class Framework
|
||||
|
||||
// 初始化必需的args参数,如果没有传入的话,使用默认值
|
||||
$this->argv = empty($argv) ? ServerStartCommand::exportOptionArray() : $argv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function init(): Framework
|
||||
{
|
||||
// 执行一些 Driver 前置条件的内容
|
||||
$this->initDriverPrerequisites();
|
||||
|
||||
@ -82,6 +87,8 @@ class Framework
|
||||
|
||||
// 初始化框架的交互以及框架部分自己要监听的事件
|
||||
$this->initFramework();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -166,7 +173,7 @@ class Framework
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
private function initDriverPrerequisites()
|
||||
public function initDriverPrerequisites()
|
||||
{
|
||||
// 寻找配置文件目录
|
||||
if ($this->argv['config-dir'] !== null) { // 如果启动参数指定了config寻找目录,那么就在指定的寻找,不在别的地方寻找了
|
||||
@ -178,7 +185,7 @@ class Framework
|
||||
foreach ($find_dir as $v) {
|
||||
if (is_dir($v)) {
|
||||
ZMConfig::setDirectory($v);
|
||||
ZMConfig::setEnv($this->argv['env'] ?? 'development');
|
||||
ZMConfig::setEnv($this->argv['env'] = $this->argv['env'] ?? 'development');
|
||||
$config_done = true;
|
||||
break;
|
||||
}
|
||||
@ -237,7 +244,7 @@ class Framework
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function initDriver()
|
||||
public function initDriver()
|
||||
{
|
||||
switch ($driver = ZMConfig::get('global.driver')) {
|
||||
case 'swoole':
|
||||
@ -263,7 +270,7 @@ class Framework
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
private function initFramework()
|
||||
public function initFramework()
|
||||
{
|
||||
// private-mode 模式下,不输出任何内容
|
||||
if (!$this->argv['private-mode']) {
|
||||
@ -276,7 +283,16 @@ class Framework
|
||||
ob_event_provider()->addEventListener(WorkerStartEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStart'], 999);
|
||||
ob_event_provider()->addEventListener(WorkerStopEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStop'], 999);
|
||||
// Http 事件
|
||||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest'], 999);
|
||||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), function () {
|
||||
global $starttime;
|
||||
$starttime = microtime(true);
|
||||
}, 1000);
|
||||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), function () {
|
||||
global $starttime;
|
||||
logger()->error('Finally used ' . round((microtime(true) - $starttime) * 1000, 4) . ' ms');
|
||||
}, 0);
|
||||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest999'], 999);
|
||||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest1'], 1);
|
||||
// manager 事件
|
||||
ob_event_provider()->addEventListener(ManagerStartEvent::getName(), [ManagerEventListener::getInstance(), 'onManagerStart'], 999);
|
||||
ob_event_provider()->addEventListener(ManagerStopEvent::getName(), [ManagerEventListener::getInstance(), 'onManagerStop'], 999);
|
||||
@ -300,7 +316,7 @@ class Framework
|
||||
// 打印工作目录
|
||||
$properties['working_dir'] = WORKING_DIR;
|
||||
// 打印环境信息
|
||||
$properties['environment'] = ($this->argv['env'] ?? null) === null ? 'default' : $this->argv['env'];
|
||||
$properties['environment'] = $this->argv['env'];
|
||||
// 打印驱动
|
||||
$properties['driver'] = ZMConfig::get('global.driver');
|
||||
// 打印logger显示等级
|
||||
@ -476,7 +492,6 @@ class Framework
|
||||
{
|
||||
if (Phar::running() !== '') {
|
||||
// 在 Phar 下,不需要新启动进程了,因为 Phar 没办法重载,自然不需要考虑多进程的加载 reload 问题
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require FRAMEWORK_ROOT_DIR . '/src/Globals/script_setup_loader.php';
|
||||
$r = _zm_setup_loader();
|
||||
$result_code = 0;
|
||||
|
||||
35
src/ZM/InstantApplication.php
Normal file
35
src/ZM/InstantApplication.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM;
|
||||
|
||||
use Exception;
|
||||
use ZM\Exception\InitException;
|
||||
use ZM\Plugin\InstantPlugin;
|
||||
|
||||
class InstantApplication extends InstantPlugin
|
||||
{
|
||||
private static $obj;
|
||||
|
||||
/**
|
||||
* @param null|mixed $dir
|
||||
* @throws InitException
|
||||
*/
|
||||
public function __construct($dir = null)
|
||||
{
|
||||
if (self::$obj !== null) {
|
||||
throw new InitException(zm_internal_errcode('E00069') . 'Initializing another Application is not allowed!');
|
||||
}
|
||||
self::$obj = $this; // 用于标记已经初始化完成
|
||||
parent::__construct($dir ?? __DIR__);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
(new Framework())->init()->start();
|
||||
}
|
||||
}
|
||||
204
src/ZM/Middleware/MiddlewareHandler.php
Normal file
204
src/ZM/Middleware/MiddlewareHandler.php
Normal file
@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Middleware;
|
||||
|
||||
use Closure;
|
||||
use OneBot\Util\Singleton;
|
||||
use Throwable;
|
||||
use ZM\Exception\InvalidArgumentException;
|
||||
|
||||
class MiddlewareHandler
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* @var array 存储中间件的
|
||||
*/
|
||||
protected $middlewares = [];
|
||||
|
||||
/**
|
||||
* @var array 存储注册中间件的类和方法
|
||||
*/
|
||||
protected $reg_map = [];
|
||||
|
||||
/**
|
||||
* @var array 用于将中间件名称压栈
|
||||
*/
|
||||
protected $stack = [];
|
||||
|
||||
/**
|
||||
* @var array 用于将正在运行的中间件压栈
|
||||
*/
|
||||
protected $callable_stack = [];
|
||||
|
||||
public function registerBefore(string $name, callable $callback)
|
||||
{
|
||||
$this->middlewares[$name]['before'] = $callback;
|
||||
}
|
||||
|
||||
public function registerAfter(string $name, callable $callback)
|
||||
{
|
||||
if (
|
||||
is_array($callback) // 如果是数组类型callback
|
||||
&& is_object($callback[0]) // 且为动态调用
|
||||
&& isset($this->middlewares[$name]['before']) // 且存在before
|
||||
&& is_array($this->middlewares[$name]['before']) // 且before也是数组类型callback
|
||||
&& is_object($this->middlewares[$name]['before'][0]) // 且before类型也为动态调用
|
||||
&& get_class($this->middlewares[$name]['before'][0]) === get_class($callback[0]) // 且before和after在一个类
|
||||
) {
|
||||
// 那么就把after的对象替换为和before同一个
|
||||
$callback[0] = $this->middlewares[$name]['before'][0];
|
||||
}
|
||||
$this->middlewares[$name]['after'] = $callback;
|
||||
}
|
||||
|
||||
public function registerException(string $name, string $exception_class, callable $callback)
|
||||
{
|
||||
$this->middlewares[$name]['exception'][$exception_class] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function bindMiddleware(callable $callback, string $name, array $params = [])
|
||||
{
|
||||
$stack_id = $this->getStackId($callback);
|
||||
// TODO: 对中间件是否存在进行检查
|
||||
if (class_exists($name)) {
|
||||
$obj = resolve($name);
|
||||
}
|
||||
|
||||
$this->reg_map[$stack_id][] = [$name, $params];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function process(callable $callback, array $args)
|
||||
{
|
||||
try {
|
||||
$before_result = MiddlewareHandler::getInstance()->processBefore($callback, $args);
|
||||
if ($before_result) {
|
||||
$result = container()->call($callback, $args);
|
||||
}
|
||||
MiddlewareHandler::getInstance()->processAfter($callback, $args);
|
||||
} catch (Throwable $e) {
|
||||
MiddlewareHandler::getInstance()->processException($callback, $args, $e);
|
||||
}
|
||||
return $result ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用中间件的前
|
||||
*
|
||||
* @param callable $callback 必须是数组形式的动态调用
|
||||
* @param array $args 参数列表
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function processBefore(callable $callback, array $args): bool
|
||||
{
|
||||
// 压栈ID
|
||||
$stack_id = $this->getStackId($callback);
|
||||
// 清除之前的
|
||||
unset($this->stack[$stack_id]);
|
||||
$this->callable_stack[] = $callback;
|
||||
// 遍历执行before并压栈,并在遇到返回false后停止
|
||||
try {
|
||||
foreach (($this->reg_map[$stack_id] ?? []) as $item) {
|
||||
$this->stack[$stack_id][] = $item;
|
||||
if (isset($this->middlewares[$item[0]]['before'])) {
|
||||
$return = container()->call($this->middlewares[$item[0]]['before'], $args);
|
||||
if ($return === false) {
|
||||
array_pop($this->callable_stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
array_pop($this->callable_stack);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正在运行的回调调用对象,可能是Closure、array、string
|
||||
*
|
||||
* @return false|mixed
|
||||
*/
|
||||
public function getCurrentCallable()
|
||||
{
|
||||
return end($this->callable_stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 调用中间件的后
|
||||
*
|
||||
* @param callable $callback 必须是数组形式的动态调用
|
||||
* @param array $args 参数列表
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function processAfter(callable $callback, array $args)
|
||||
{
|
||||
// 压栈ID
|
||||
$stack_id = $this->getStackId($callback);
|
||||
// 从栈内倒序取出已经执行过的中间件,并执行after
|
||||
$this->callable_stack[] = $callback;
|
||||
try {
|
||||
while (isset($this->stack[$stack_id]) && ($item = array_pop($this->stack[$stack_id])) !== null) {
|
||||
if (isset($this->middlewares[$item[0]]['after'])) {
|
||||
container()->call($this->middlewares[$item[0]]['after'], $args);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
array_pop($this->callable_stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 调用中间件的异常捕获处理
|
||||
*
|
||||
* @param callable $callback 必须是数组形式的动态调用
|
||||
* @param array $args 参数列表
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function processException(callable $callback, array $args, Throwable $throwable)
|
||||
{
|
||||
// 压栈ID
|
||||
$stack_id = $this->getStackId($callback);
|
||||
// 从栈内倒序取出已经执行过的中间件,并执行after
|
||||
while (isset($this->stack[$stack_id]) && ($item = array_pop($this->stack[$stack_id])) !== null) {
|
||||
foreach ($this->middlewares[$item[0]]['exception'] as $k => $v) {
|
||||
if (is_a($throwable, $k)) {
|
||||
$v($throwable, ...$args);
|
||||
unset($this->stack[$stack_id]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw $throwable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $callback 可执行的方法
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getStackId(callable $callback): string
|
||||
{
|
||||
if ($callback instanceof Closure) {
|
||||
// 闭包情况下,直接根据闭包的ID号来找stack
|
||||
return strval(spl_object_id($callback));
|
||||
}
|
||||
if (is_array($callback) && count($callback) === 2) {
|
||||
// 活性调用,根据组合名称来判断
|
||||
return (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1];
|
||||
}
|
||||
if (is_string($callback)) {
|
||||
return $callback;
|
||||
}
|
||||
throw new InvalidArgumentException('传入的 callable 有误!');
|
||||
}
|
||||
}
|
||||
9
src/ZM/Middleware/MiddlewareInterface.php
Normal file
9
src/ZM/Middleware/MiddlewareInterface.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Middleware;
|
||||
|
||||
interface MiddlewareInterface
|
||||
{
|
||||
}
|
||||
28
src/ZM/Middleware/TimerMiddleware.php
Normal file
28
src/ZM/Middleware/TimerMiddleware.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Middleware;
|
||||
|
||||
class TimerMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/** @var float */
|
||||
private $starttime = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
middleware()->registerBefore(static::class, [$this, 'onBefore']);
|
||||
middleware()->registerAfter(static::class, [$this, 'onAfter']);
|
||||
}
|
||||
|
||||
public function onBefore(): bool
|
||||
{
|
||||
$this->starttime = microtime(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function onAfter()
|
||||
{
|
||||
logger()->info('Using ' . round((microtime(true) - $this->starttime) * 1000, 4) . ' ms');
|
||||
}
|
||||
}
|
||||
77
src/ZM/Plugin/InstantPlugin.php
Normal file
77
src/ZM/Plugin/InstantPlugin.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Plugin;
|
||||
|
||||
use ZM\Annotation\Http\Route;
|
||||
use ZM\Annotation\OneBot\BotCommand;
|
||||
use ZM\Annotation\OneBot\BotEvent;
|
||||
|
||||
class InstantPlugin
|
||||
{
|
||||
/** @var string 插件目录 */
|
||||
protected $dir;
|
||||
|
||||
/** @var array 机器人事件列表 */
|
||||
protected $bot_events = [];
|
||||
|
||||
/** @var array 机器人指令列表 */
|
||||
protected $bot_commands = [];
|
||||
|
||||
/** @var array 全局的事件列表 */
|
||||
protected $events = [];
|
||||
|
||||
/** @var array 注册的路由列表 */
|
||||
protected $routes = [];
|
||||
|
||||
public function __construct(string $dir)
|
||||
{
|
||||
$this->dir = $dir;
|
||||
}
|
||||
|
||||
public function getDir(): string
|
||||
{
|
||||
return $this->dir;
|
||||
}
|
||||
|
||||
public function addBotEvent(BotEvent $event)
|
||||
{
|
||||
$this->bot_events[] = $event;
|
||||
}
|
||||
|
||||
public function addBotCommand(BotCommand $command)
|
||||
{
|
||||
$this->bot_commands[] = $command;
|
||||
}
|
||||
|
||||
public function registerEvent(string $event_name, callable $callback, int $level = 20)
|
||||
{
|
||||
$this->events[] = [$event_name, $callback, $level];
|
||||
}
|
||||
|
||||
public function addHttpRoute(Route $route)
|
||||
{
|
||||
$this->routes[] = $route;
|
||||
}
|
||||
|
||||
public function getBotEvents(): array
|
||||
{
|
||||
return $this->bot_events;
|
||||
}
|
||||
|
||||
public function getBotCommands(): array
|
||||
{
|
||||
return $this->bot_commands;
|
||||
}
|
||||
|
||||
public function getEvents(): array
|
||||
{
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
public function getRoutes(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace ZM\Store;
|
||||
|
||||
use RuntimeException;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
class FileSystem
|
||||
{
|
||||
@ -113,7 +114,7 @@ class FileSystem
|
||||
public static function getClassesPsr4(string $dir, string $base_namespace, $rule = null, $return_path_value = false): array
|
||||
{
|
||||
// 预先读取下composer的file列表
|
||||
$composer = json_decode(file_get_contents(zm_dir(SOURCE_ROOT_DIR . '/composer.json')), true);
|
||||
$composer = ZMUtil::getComposerMetadata();
|
||||
$classes = [];
|
||||
// 扫描目录,使用递归模式,相对路径模式,因为下面此路径要用作转换成namespace
|
||||
$files = FileSystem::scanDirFiles($dir, true, true);
|
||||
@ -142,7 +143,7 @@ class FileSystem
|
||||
/*if (substr(file_get_contents($dir . '/' . $v), 6, 6) == '#plain') {
|
||||
continue;
|
||||
}*/
|
||||
if (file_exists($dir . '/' . $pathinfo['basename'] . '.plain')) {
|
||||
if (file_exists($dir . '/' . $pathinfo['basename'] . '.ignore')) {
|
||||
continue;
|
||||
}
|
||||
if (mb_substr($pathinfo['basename'], 0, 7) == 'global_' || mb_substr($pathinfo['basename'], 0, 7) == 'script_') {
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Store;
|
||||
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* 框架内部使用的全局变量
|
||||
*/
|
||||
class InternalGlobals
|
||||
{
|
||||
/**
|
||||
* @var null|RouteCollection 用于保存 Route 注解的路由树
|
||||
* @internal
|
||||
*/
|
||||
public static $routes;
|
||||
}
|
||||
170
src/ZM/Utils/HttpUtil.php
Normal file
170
src/ZM/Utils/HttpUtil.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
use OneBot\Http\HttpFactory;
|
||||
use OneBot\Http\ServerRequest;
|
||||
use OneBot\Http\Stream;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
|
||||
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
|
||||
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Store\FileSystem;
|
||||
|
||||
/**
|
||||
* Http 工具类
|
||||
*/
|
||||
class HttpUtil
|
||||
{
|
||||
/**
|
||||
* @var RouteCollection
|
||||
*/
|
||||
private static $routes;
|
||||
|
||||
/**
|
||||
* 解析 Uri,用于匹配路由用的
|
||||
* 返回值为状态
|
||||
* 第二个参数为路由节点
|
||||
* 第三个参数为动态路由节点中匹配到的参数列表
|
||||
*
|
||||
* @param mixed $node
|
||||
* @param mixed $params
|
||||
*/
|
||||
public static function parseUri(ServerRequest $request, &$node, &$params): int
|
||||
{
|
||||
// 建立上下文,设置当前请求的方法
|
||||
$context = new RequestContext();
|
||||
$context->setMethod($request->getMethod());
|
||||
|
||||
try {
|
||||
// 使用UrlMatcher进行匹配Url
|
||||
$matcher = new UrlMatcher(static::getRouteCollection(), $context);
|
||||
$matched = $matcher->match($request->getUri()->getPath());
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
// 路由找不到会抛出异常,我们不需要这个异常,转换为状态码
|
||||
return ZM_ERR_ROUTE_NOT_FOUND;
|
||||
} catch (MethodNotAllowedException $e) {
|
||||
// 路由匹配到了,但该路由不能使用该方法,所以返回状态码(路由不允许)
|
||||
return ZM_ERR_ROUTE_METHOD_NOT_ALLOWED;
|
||||
}
|
||||
// 匹配到的时候,matched不为空
|
||||
if (!empty($matched)) {
|
||||
$node = [
|
||||
'route' => static::getRouteCollection()->get($matched['_route'])->getPath(),
|
||||
'class' => $matched['_class'],
|
||||
'method' => $matched['_method'],
|
||||
'request_method' => $request->getMethod(),
|
||||
];
|
||||
unset($matched['_class'], $matched['_method']);
|
||||
$params = $matched;
|
||||
// 返回成功的状态码
|
||||
return ZM_ERR_NONE;
|
||||
}
|
||||
// 返回没有匹配到的状态码
|
||||
return ZM_ERR_ROUTE_NOT_FOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析返回静态文件
|
||||
*
|
||||
* @params string $uri 路由地址
|
||||
* @params string $settings 动态传入的配置模式
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public static function handleStaticPage(string $uri, array $settings = []): ResponseInterface
|
||||
{
|
||||
// 确定根目录
|
||||
$base_dir = $settings['document_root'] ?? ZMConfig::get('global.file_server.document_root');
|
||||
// 将相对路径转换为绝对路径
|
||||
if (FileSystem::isRelativePath($base_dir)) {
|
||||
$base_dir = SOURCE_ROOT_DIR . '/' . $base_dir;
|
||||
}
|
||||
// 支持默认缺省搜索的文件名(如index.html)
|
||||
$base_index = $settings['document_index'] ?? ZMConfig::get('global.file_server.document_index');
|
||||
if (is_string($base_index)) {
|
||||
$base_index = [$base_index];
|
||||
}
|
||||
$path = realpath($base_dir . urldecode($uri));
|
||||
if ($path !== false) {
|
||||
// 安全问题,防止目录穿越,只能囚禁到规定的 Web 根目录下获取文件
|
||||
$work = realpath($base_dir) . '/';
|
||||
if (strpos($path, $work) !== 0) {
|
||||
logger()->info('[403] ' . $uri);
|
||||
return static::handleHttpCodePage(403);
|
||||
}
|
||||
// 如果路径是文件夹的话,如果结尾没有 /,则自动302补充,和传统的Nginx效果相同
|
||||
if (is_dir($path)) {
|
||||
if (mb_substr($uri, -1, 1) != '/') {
|
||||
logger()->info('[302] ' . $uri);
|
||||
return HttpFactory::getInstance()->createResponse(302, null, ['Location' => $uri . '/']);
|
||||
}
|
||||
// 如果结尾有 /,那么就根据默认搜索的文件名进行搜索文件是否存在,存在则直接返回对应文件
|
||||
foreach ($base_index as $vp) {
|
||||
if (is_file($path . '/' . $vp)) {
|
||||
logger()->info('[200] ' . $uri);
|
||||
$exp = strtolower(pathinfo($path . $vp)['extension'] ?? 'unknown');
|
||||
return HttpFactory::getInstance()->createResponse()
|
||||
->withAddedHeader('Content-Type', ZMConfig::get('file_header')[$exp] ?? 'application/octet-stream')
|
||||
->withBody(HttpFactory::getInstance()->createStream(file_get_contents($path . '/' . $vp)));
|
||||
}
|
||||
}
|
||||
// 如果文件存在,则直接返回文件内容
|
||||
} elseif (is_file($path)) {
|
||||
logger()->info('[200] ' . $uri);
|
||||
$exp = strtolower(pathinfo($path)['extension'] ?? 'unknown');
|
||||
return HttpFactory::getInstance()->createResponse()
|
||||
->withAddedHeader('Content-Type', ZMConfig::get('file_header')[$exp] ?? 'application/octet-stream')
|
||||
->withBody(HttpFactory::getInstance()->createStream(file_get_contents($path)));
|
||||
}
|
||||
}
|
||||
// 否则最终肯定只能返回 404 了
|
||||
logger()->info('[404] ' . $uri);
|
||||
return static::handleHttpCodePage(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动寻找默认的 HTTP Code 页面
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public static function handleHttpCodePage(int $code): ResponseInterface
|
||||
{
|
||||
// 获取有没有规定 code page
|
||||
$code_page = ZMConfig::get('global.file_server.document_code_page')[$code] ?? null;
|
||||
if ($code_page !== null && !file_exists((ZMConfig::get('global.file_server.document_root') ?? '/not/exist/') . '/' . $code_page)) {
|
||||
$code_page = null;
|
||||
}
|
||||
if ($code_page === null) {
|
||||
return HttpFactory::getInstance()->createResponse($code);
|
||||
}
|
||||
return HttpFactory::getInstance()->createResponse($code, null, [], file_get_contents(ZMConfig::get('global.file_server.document_root') . '/' . $code_page));
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速创建一个 JSON 格式的 HTTP 响应
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @param int $http_code HTTP 状态码
|
||||
* @param int $json_flag JSON 编码时传入的flag
|
||||
*/
|
||||
public static function createJsonResponse(array $data, int $http_code = 200, int $json_flag = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE): ResponseInterface
|
||||
{
|
||||
return HttpFactory::getInstance()->createResponse($http_code)
|
||||
->withAddedHeader('Content-Type', 'application/json')
|
||||
->withBody(Stream::create(json_encode($data, $json_flag)));
|
||||
}
|
||||
|
||||
public static function getRouteCollection(): RouteCollection
|
||||
{
|
||||
if (self::$routes === null) {
|
||||
self::$routes = new RouteCollection();
|
||||
}
|
||||
return self::$routes;
|
||||
}
|
||||
}
|
||||
123
src/ZM/Utils/ReflectionUtil.php
Normal file
123
src/ZM/Utils/ReflectionUtil.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
use Closure;
|
||||
use ReflectionException;
|
||||
use ReflectionFunction;
|
||||
use ReflectionFunctionAbstract;
|
||||
use ReflectionMethod;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionParameter;
|
||||
|
||||
class ReflectionUtil
|
||||
{
|
||||
/**
|
||||
* 获取参数的类名(如有)
|
||||
*
|
||||
* @param ReflectionParameter $parameter 参数
|
||||
* @return null|string 类名,如果参数不是类,返回 null
|
||||
*/
|
||||
public static function getParameterClassName(ReflectionParameter $parameter): ?string
|
||||
{
|
||||
// 获取参数类型
|
||||
$type = $parameter->getType();
|
||||
// 没有声明类型或为基本类型
|
||||
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取类名
|
||||
$class_name = $type->getName();
|
||||
|
||||
// 如果存在父类
|
||||
if (!is_null($class = $parameter->getDeclaringClass())) {
|
||||
if ($class_name === 'self') {
|
||||
return $class->getName();
|
||||
}
|
||||
|
||||
if ($class_name === 'parent' && $parent = $class->getParentClass()) {
|
||||
return $parent->getName();
|
||||
}
|
||||
}
|
||||
|
||||
return $class_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将传入变量转换为字符串
|
||||
*
|
||||
* @param mixed $var
|
||||
*/
|
||||
public static function variableToString($var): string
|
||||
{
|
||||
switch (true) {
|
||||
case is_callable($var):
|
||||
if (is_array($var)) {
|
||||
if (is_object($var[0])) {
|
||||
return get_class($var[0]) . '@' . $var[1];
|
||||
}
|
||||
return $var[0] . '::' . $var[1];
|
||||
}
|
||||
return 'closure';
|
||||
case is_string($var):
|
||||
return $var;
|
||||
case is_array($var):
|
||||
return 'array' . json_encode($var);
|
||||
case is_object($var):
|
||||
return get_class($var);
|
||||
case is_resource($var):
|
||||
return 'resource(' . get_resource_type($var) . ')';
|
||||
case is_null($var):
|
||||
return 'null';
|
||||
case is_bool($var):
|
||||
return $var ? 'true' : 'false';
|
||||
case is_float($var):
|
||||
case is_int($var):
|
||||
return (string) $var;
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断传入的回调是否为任意类的非静态方法
|
||||
*
|
||||
* @param callable|string $callback 回调
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public static function isNonStaticMethod($callback): bool
|
||||
{
|
||||
if (is_array($callback) && is_string($callback[0])) {
|
||||
$reflection = new ReflectionMethod($callback[0], $callback[1]);
|
||||
return !$reflection->isStatic();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传入的回调的反射实例
|
||||
*
|
||||
* 如果传入的是类方法,则会返回 {@link ReflectionMethod} 实例
|
||||
* 否则将返回 {@link ReflectionFunction} 实例
|
||||
*
|
||||
* 可传入实现了 __invoke 的类
|
||||
*
|
||||
* @param callable|string $callback 回调
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public static function getCallReflector($callback): ReflectionFunctionAbstract
|
||||
{
|
||||
if (is_string($callback) && str_contains($callback, '::')) {
|
||||
$callback = explode('::', $callback);
|
||||
} elseif (is_object($callback) && !$callback instanceof Closure) {
|
||||
$callback = [$callback, '__invoke'];
|
||||
}
|
||||
|
||||
return is_array($callback)
|
||||
? new ReflectionMethod($callback[0], $callback[1])
|
||||
: new ReflectionFunction($callback);
|
||||
}
|
||||
}
|
||||
@ -6,4 +6,11 @@ namespace ZM\Utils;
|
||||
|
||||
class ZMUtil
|
||||
{
|
||||
/**
|
||||
* 获取 composer.json 并转为数组进行读取使用
|
||||
*/
|
||||
public static function getComposerMetadata(): ?array
|
||||
{
|
||||
return json_decode(file_get_contents(SOURCE_ROOT_DIR . '/composer.json'), true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// CLI Application 入口文件,先引入 Composer 组件
|
||||
use OneBot\Driver\ExceptionHandler;
|
||||
|
||||
/**
|
||||
* CLI Application 入口文件,先引入 Composer 组件
|
||||
*
|
||||
* @noinspection PhpIncludeInspection
|
||||
*/
|
||||
require_once((!is_dir(__DIR__ . '/../vendor')) ? getcwd() : (__DIR__ . '/..')) . '/vendor/autoload.php';
|
||||
|
||||
// 适配 Windows 的 conhost 中文显示,因为使用 micro 打包框架运行的时候在 Windows 运行中文部分会变成乱码
|
||||
@ -11,4 +17,9 @@ if (DIRECTORY_SEPARATOR === '\\') {
|
||||
}
|
||||
|
||||
// 开始运行,运行 symfony console 组件并解析命令
|
||||
(new ZM\ConsoleApplication('zhamao-framework'))->run();
|
||||
try {
|
||||
(new ZM\ConsoleApplication('zhamao-framework'))->run();
|
||||
} catch (Exception $e) {
|
||||
ExceptionHandler::getInstance()->handle($e);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@ -6,11 +6,9 @@ namespace Tests\ZM\Utils;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Swoole\Http\Request;
|
||||
use Swoole\Http\Response;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
use ZM\Annotation\Http\RequestMethod;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Utils\HttpUtil;
|
||||
use ZM\Utils\Manager\RouteManager;
|
||||
|
||||
@ -19,17 +17,6 @@ use ZM\Utils\Manager\RouteManager;
|
||||
*/
|
||||
class HttpUtilTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider providerTestHandleStaticPage
|
||||
*/
|
||||
public function testHandleStaticPage(string $page, bool $expected): void
|
||||
{
|
||||
$swoole_response = $this->getMockClass(Response::class);
|
||||
$r = new \ZM\Http\Response(new $swoole_response());
|
||||
HttpUtil::handleStaticPage($page, $r, ZMConfig::get('global', 'static_file_server'));
|
||||
$this->assertEquals($expected, $r->getStatusCode() === 200);
|
||||
}
|
||||
|
||||
public function providerTestHandleStaticPage(): array
|
||||
{
|
||||
return [
|
||||
@ -38,17 +25,6 @@ class HttpUtilTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \ZM\Utils\HttpUtil::getHttpCodePage
|
||||
* @covers \ZM\Utils\HttpUtil::responseCodePage
|
||||
* @dataProvider providerTestGetHttpCodePage
|
||||
*/
|
||||
public function testGetHttpCodePage(int $code, bool $expected): void
|
||||
{
|
||||
$has_response = !empty(HttpUtil::getHttpCodePage($code));
|
||||
$this->assertSame($expected, $has_response);
|
||||
}
|
||||
|
||||
public function providerTestGetHttpCodePage(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user