refactor all base things

This commit is contained in:
crazywhalecc 2022-08-13 17:00:29 +08:00 committed by Jerry Ma
parent 1c801bb205
commit b2c95d96b1
54 changed files with 3009 additions and 383 deletions

View File

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

View File

@ -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"
}
},

View File

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

View File

@ -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
View 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
View 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();

View File

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

View File

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

View File

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

View File

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

View 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 ZhamaoThis is the first 3.0 page';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View 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 !');
}
}

View File

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

View File

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

View File

@ -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运行状态

View 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);
}
}
}

View 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);
}
}

View 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);
}

View 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']);
}
}

View 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);
}
}

View 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
{
}

View 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
{
}

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

View 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完善上下文的方法们
}

View 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);
}

View 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);
}
}

View File

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

View File

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

View File

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

View 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);
}
}

View File

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

View 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();
}
}

View 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 有误!');
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace ZM\Middleware;
interface MiddlewareInterface
{
}

View 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');
}
}

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

View File

@ -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_') {

View File

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

View 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);
}
}

View File

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

View File

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

View File

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