Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48298ef1f7 | |||
| d4a1e9c8d7 | |||
| f9ac567bfe | |||
| be7c5da071 | |||
| fec0715c40 | |||
| 2239a34681 | |||
| 3daa4021ec | |||
| 7455dc8d58 | |||
| 5a1aa2d60f | |||
| e40db9d1fb | |||
| b5e0026816 | |||
| 69804ea595 | |||
| f099ca8402 | |||
| 0087115bdc | |||
| fead1203be | |||
| 28973df8b6 | |||
| 4cfd04c988 |
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace PHPSTORM_META {
|
||||
registerArgumentsSet(
|
||||
'router_actions',
|
||||
\App\Controller\SiteController::class . '@globSetting'
|
||||
);
|
||||
|
||||
expectedArguments(\Kiri\Router\Router::get(), 1, argumentsSet('router_actions'));
|
||||
expectedArguments(\Kiri\Router\Router::post(), 1, argumentsSet('router_actions'));
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.5",
|
||||
"composer-runtime-api": "^2.0",
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"psr/http-message": "^1.0"
|
||||
|
||||
Generated
+233
@@ -0,0 +1,233 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "3a858a1006b21751883a8dcfbd184549",
|
||||
"packages": [
|
||||
{
|
||||
"name": "lovefc/eztpl",
|
||||
"version": "1.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lovefc/eztpl.git",
|
||||
"reference": "bb45c458b7522fae3fd6a5687db7b7d227c41b43"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lovefc/eztpl/zipball/bb45c458b7522fae3fd6a5687db7b7d227c41b43",
|
||||
"reference": "bb45c458b7522fae3fd6a5687db7b7d227c41b43",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"lovefc\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "lovefc",
|
||||
"homepage": "http://lovefc.cn"
|
||||
}
|
||||
],
|
||||
"description": "一个小型高效的PHP单文件模板引擎",
|
||||
"homepage": "https://github.com/lovefc/eztpl.git",
|
||||
"keywords": [
|
||||
"php",
|
||||
"template"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/lovefc/eztpl/issues",
|
||||
"source": "https://github.com/lovefc/eztpl/tree/1.7.0"
|
||||
},
|
||||
"time": "2023-10-21T06:04:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
|
||||
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
"homepage": "https://github.com/php-fig/http-message",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-message/tree/1.1"
|
||||
},
|
||||
"time": "2023-04-04T09:50:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-server-handler",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-server-handler.git",
|
||||
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
|
||||
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Server\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP server-side request handler",
|
||||
"keywords": [
|
||||
"handler",
|
||||
"http",
|
||||
"http-interop",
|
||||
"psr",
|
||||
"psr-15",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response",
|
||||
"server"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
|
||||
},
|
||||
"time": "2023-04-10T20:06:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-server-middleware",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-server-middleware.git",
|
||||
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
|
||||
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"psr/http-message": "^1.0 || ^2.0",
|
||||
"psr/http-server-handler": "^1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Server\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP server-side middleware",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-interop",
|
||||
"middleware",
|
||||
"psr",
|
||||
"psr-15",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-fig/http-server-middleware/issues",
|
||||
"source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
|
||||
},
|
||||
"time": "2023-04-11T06:14:47+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=8.4",
|
||||
"composer-runtime-api": "^2.0"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.2.0"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Router\Annotate;
|
||||
|
||||
use Kiri\Di\Interface\InjectMethodInterface;
|
||||
use Kiri\Router\Defer\DeferRegistry;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class Defer implements InjectMethodInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string|array $callback Class name or [Class::class, 'method']
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(readonly public string|array $callback, readonly public array $params = [])
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function dispatch(string $class, string $method): void
|
||||
{
|
||||
DeferRegistry::add($class, $method, $this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -97,7 +97,8 @@ class ConstrictResponse extends Message implements ResponseInterface
|
||||
*/
|
||||
public function json(array $content, int $statusCode = 200): static
|
||||
{
|
||||
$this->stream->write(json_encode($content, JSON_UNESCAPED_UNICODE));
|
||||
$encoded = json_encode($content, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
$this->stream->write($encoded === false ? '{"error":"json encode failed"}' : $encoded);
|
||||
return $this->withContentType(ContentType::JSON)->withStatus($statusCode);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,4 +25,21 @@ class DataGrip
|
||||
}
|
||||
|
||||
|
||||
public function reset(?string $type = null): void
|
||||
{
|
||||
if ($type === null) {
|
||||
foreach ($this->servers as $server) {
|
||||
if ($server instanceof RouterCollector) {
|
||||
$server->clear();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($this->servers[$type])) {
|
||||
$this->servers[$type]->clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Router\Defer;
|
||||
|
||||
use Kiri;
|
||||
use Kiri\Router\Annotate\Defer;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use ReflectionClass;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
/**
|
||||
* Defer 回调执行器 — 统一处理协程安全的上下文注入与异步执行
|
||||
*/
|
||||
class DeferExecutor
|
||||
{
|
||||
|
||||
/**
|
||||
* 执行一批 Defer 回调
|
||||
*
|
||||
* @param Defer[] $defers
|
||||
*/
|
||||
public static function run(array $defers): void
|
||||
{
|
||||
if (empty($defers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = self::captureRequest();
|
||||
$response = self::captureResponse();
|
||||
|
||||
if (Coroutine::getCid() <= 0) {
|
||||
self::executeSync($defers, $request, $response);
|
||||
return;
|
||||
}
|
||||
|
||||
Coroutine::create(function () use ($defers, $request, $response) {
|
||||
self::executeSync($defers, $request, $response);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 为实例注入 request/response 上下文
|
||||
*/
|
||||
public static function inject(object $instance): object
|
||||
{
|
||||
try {
|
||||
$request = self::captureRequest();
|
||||
if ($request !== null) {
|
||||
self::setProperty($instance, 'request', $request);
|
||||
}
|
||||
|
||||
$response = self::captureResponse();
|
||||
if ($response !== null) {
|
||||
self::setProperty($instance, 'response', $response);
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
|
||||
private static function executeSync(
|
||||
array $defers,
|
||||
?ServerRequestInterface $request,
|
||||
?ResponseInterface $response
|
||||
): void {
|
||||
foreach ($defers as $defer) {
|
||||
try {
|
||||
self::invokeDefer($defer, $request, $response);
|
||||
} catch (\Throwable $throwable) {
|
||||
\Kiri::getLogger()->error('Defer callback failed: ' . $throwable->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static function invokeDefer(
|
||||
Defer $defer,
|
||||
?ServerRequestInterface $request,
|
||||
?ResponseInterface $response
|
||||
): void {
|
||||
$callback = $defer->callback;
|
||||
$params = $defer->params;
|
||||
|
||||
if (is_array($callback)) {
|
||||
[$class, $method] = $callback;
|
||||
$instance = self::resolveInstance($class, $request, $response);
|
||||
call_user_func([$instance, $method], ...$params);
|
||||
} else {
|
||||
$instance = self::resolveInstance($callback, $request, $response);
|
||||
call_user_func([$instance, '__invoke'], ...$params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static function resolveInstance(
|
||||
string $class,
|
||||
?ServerRequestInterface $request,
|
||||
?ResponseInterface $response
|
||||
): object {
|
||||
$instance = Kiri::getDi()->get($class);
|
||||
|
||||
if ($instance instanceof DeferHandler) {
|
||||
if ($request !== null) {
|
||||
$instance->request = $request;
|
||||
}
|
||||
if ($response !== null) {
|
||||
$instance->response = $response;
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
if ($request !== null) {
|
||||
self::setProperty($instance, 'request', $request);
|
||||
}
|
||||
if ($response !== null) {
|
||||
self::setProperty($instance, 'response', $response);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
|
||||
private static function setProperty(object $instance, string $name, mixed $value): void
|
||||
{
|
||||
try {
|
||||
$reflect = new ReflectionClass($instance);
|
||||
if (!$reflect->hasProperty($name)) return;
|
||||
$prop = $reflect->getProperty($name);
|
||||
if ($prop->isStatic()) return;
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue($instance, $value);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static function captureRequest(): ?ServerRequestInterface
|
||||
{
|
||||
try {
|
||||
if (function_exists('request')) return \request();
|
||||
} catch (\Throwable) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static function captureResponse(): ?ResponseInterface
|
||||
{
|
||||
try {
|
||||
if (function_exists('response')) return \response();
|
||||
} catch (\Throwable) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Router\Defer;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Defer 回调基类 — 提供 request/response 上下文属性
|
||||
*
|
||||
* 所有需要在 #[Defer] 回调中访问请求上下文的类应继承此类。
|
||||
* DeferExecutor 会自动将父协程的 request/response 注入到这两个属性。
|
||||
*/
|
||||
abstract class DeferHandler
|
||||
{
|
||||
|
||||
/** @var ServerRequestInterface 当前请求上下文 (DeferExecutor 自动注入) */
|
||||
public ServerRequestInterface $request;
|
||||
|
||||
/** @var ResponseInterface 当前响应上下文 (DeferExecutor 自动注入) */
|
||||
public ResponseInterface $response;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Router\Defer;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\PrettyPrinter\Standard;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
class DeferProxyGenerator
|
||||
{
|
||||
|
||||
private static ?string $cacheDir = null;
|
||||
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public static function create(string $className, array $construct): object
|
||||
{
|
||||
$proxyClass = $className . '__DeferProxy';
|
||||
$cacheFile = self::getCacheFile($className);
|
||||
|
||||
if (!class_exists($proxyClass, false)) {
|
||||
if ($cacheFile !== null && file_exists($cacheFile)) {
|
||||
require_once $cacheFile;
|
||||
}
|
||||
|
||||
if (!class_exists($proxyClass, false)) {
|
||||
$code = self::generate($className);
|
||||
if ($cacheFile !== null) {
|
||||
$dir = dirname($cacheFile);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
file_put_contents($cacheFile, '<?php ' . $code);
|
||||
require_once $cacheFile;
|
||||
} else {
|
||||
eval($code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists($proxyClass, false)) {
|
||||
$reflect = new ReflectionClass($proxyClass);
|
||||
return $reflect->newInstanceArgs($construct);
|
||||
}
|
||||
|
||||
$reflect = new ReflectionClass($className);
|
||||
return $reflect->newInstanceArgs($construct);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
private static function generate(string $className): string
|
||||
{
|
||||
$reflect = new ReflectionClass($className);
|
||||
$methods = DeferRegistry::getAll($className);
|
||||
$stmts = [];
|
||||
|
||||
foreach ($methods as $methodName => $defers) {
|
||||
if (!$reflect->hasMethod($methodName)) continue;
|
||||
$method = $reflect->getMethod($methodName);
|
||||
if ($method->isPrivate() || $method->isStatic() || $method->isFinal() || $method->isConstructor() || $method->isDestructor()) continue;
|
||||
$stmts[] = self::buildMethod($method);
|
||||
}
|
||||
|
||||
if (empty($stmts)) return '';
|
||||
|
||||
$classNode = new Stmt\Class_(
|
||||
new Name($className . '__DeferProxy'),
|
||||
['extends' => new Name\FullyQualified($className), 'stmts' => $stmts]
|
||||
);
|
||||
|
||||
$namespace = $reflect->getNamespaceName();
|
||||
$namespaceNode = new Stmt\Namespace_(
|
||||
$namespace !== '' ? new Name($namespace) : null,
|
||||
[$classNode]
|
||||
);
|
||||
|
||||
$printer = new Standard();
|
||||
return $printer->prettyPrintFile([$namespaceNode]);
|
||||
}
|
||||
|
||||
|
||||
private static function buildMethod(ReflectionMethod $method): Stmt\ClassMethod
|
||||
{
|
||||
$methodName = $method->getName();
|
||||
$params = [];
|
||||
$args = [];
|
||||
|
||||
foreach ($method->getParameters() as $param) {
|
||||
$type = null;
|
||||
$refType = $param->getType();
|
||||
if ($refType instanceof \ReflectionNamedType) {
|
||||
$type = new Name($refType->getName());
|
||||
}
|
||||
|
||||
$default = null;
|
||||
if ($param->isDefaultValueAvailable()) {
|
||||
$default = self::buildDefaultValue($param);
|
||||
}
|
||||
|
||||
$var = new Expr\Variable($param->getName());
|
||||
|
||||
$params[] = new Node\Param($var, $default, $type,
|
||||
byRef: $param->isPassedByReference(),
|
||||
variadic: $param->isVariadic()
|
||||
);
|
||||
|
||||
$args[] = new Node\Arg($var,
|
||||
byRef: $param->isPassedByReference(),
|
||||
unpack: $param->isVariadic()
|
||||
);
|
||||
}
|
||||
|
||||
$returnType = null;
|
||||
$refReturnType = $method->getReturnType();
|
||||
if ($refReturnType instanceof \ReflectionNamedType) {
|
||||
$returnType = new Name($refReturnType->getName());
|
||||
}
|
||||
|
||||
$stmts = [
|
||||
new Stmt\Expression(new Expr\Assign(new Expr\Variable('result'),
|
||||
new Expr\StaticCall(new Name('parent'), $methodName, $args))),
|
||||
new Stmt\Expression(
|
||||
new Expr\StaticCall(
|
||||
new Name\FullyQualified(DeferRegistry::class), 'execute', [
|
||||
new Node\Arg(new Expr\ClassConstFetch(new Name\FullyQualified($method->getDeclaringClass()->getName()), 'class')),
|
||||
new Node\Arg(new Node\Scalar\String_($methodName)),
|
||||
]
|
||||
)
|
||||
),
|
||||
new Stmt\Return_(new Expr\Variable('result')),
|
||||
];
|
||||
|
||||
return new Stmt\ClassMethod($methodName, [
|
||||
'flags' => $method->isPublic() ? Stmt\Class_::MODIFIER_PUBLIC : Stmt\Class_::MODIFIER_PROTECTED,
|
||||
'params' => $params,
|
||||
'returnType' => $returnType,
|
||||
'stmts' => $stmts,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
private static function buildDefaultValue(\ReflectionParameter $param): Node\Expr
|
||||
{
|
||||
if (!$param->isDefaultValueAvailable()) {
|
||||
return new Expr\ConstFetch(new Name('null'));
|
||||
}
|
||||
$value = $param->getDefaultValue();
|
||||
return match (true) {
|
||||
is_bool($value) => new Expr\ConstFetch(new Name($value ? 'true' : 'false')),
|
||||
is_int($value) => new Node\Scalar\LNumber($value),
|
||||
is_float($value) => new Node\Scalar\DNumber($value),
|
||||
is_string($value) => new Node\Scalar\String_($value),
|
||||
is_array($value) => new Expr\Array_(array_map(
|
||||
fn($k, $v) => new Expr\ArrayItem(
|
||||
self::buildDefaultValueFromScalar($v),
|
||||
is_string($k) ? new Node\Scalar\String_($k) : null
|
||||
), array_keys($value), $value
|
||||
)),
|
||||
default => new Expr\ConstFetch(new Name('null')),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static function buildDefaultValueFromScalar(mixed $value): Node\Expr
|
||||
{
|
||||
return match (true) {
|
||||
is_bool($value) => new Expr\ConstFetch(new Name($value ? 'true' : 'false')),
|
||||
is_int($value) => new Node\Scalar\LNumber($value),
|
||||
is_float($value) => new Node\Scalar\DNumber($value),
|
||||
is_string($value) => new Node\Scalar\String_($value),
|
||||
default => new Expr\ConstFetch(new Name('null')),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static function getCacheFile(string $className): ?string
|
||||
{
|
||||
if (self::$cacheDir === null) {
|
||||
if (defined('APP_PATH')) {
|
||||
self::$cacheDir = APP_PATH . 'runtime/proxies/';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return self::$cacheDir . str_replace('\\', '_', $className) . '__DeferProxy.php';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Router\Defer;
|
||||
|
||||
use Kiri\Router\Annotate\Defer;
|
||||
|
||||
class DeferRegistry
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array<string, Defer[]> "ClassName::method" => Defer[]
|
||||
*/
|
||||
private static array $registry = [];
|
||||
|
||||
|
||||
public static function add(string $class, string $method, Defer $defer): void
|
||||
{
|
||||
$key = self::key($class, $method);
|
||||
self::$registry[$key][] = $defer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Defer[]
|
||||
*/
|
||||
public static function get(string $class, string $method): array
|
||||
{
|
||||
return self::$registry[self::key($class, $method)] ?? [];
|
||||
}
|
||||
|
||||
|
||||
public static function hasAny(string $class): bool
|
||||
{
|
||||
$prefix = $class . '::';
|
||||
foreach (array_keys(self::$registry) as $key) {
|
||||
if (str_starts_with($key, $prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array<string, Defer[]> method => Defer[]
|
||||
*/
|
||||
public static function getAll(string $class): array
|
||||
{
|
||||
$result = [];
|
||||
$prefix = $class . '::';
|
||||
foreach (self::$registry as $key => $defers) {
|
||||
if (str_starts_with($key, $prefix)) {
|
||||
$method = substr($key, strlen($prefix));
|
||||
$result[$method] = $defers;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 异步执行 Defer 回调 — 委托 DeferExecutor 处理协程安全与上下文注入
|
||||
*/
|
||||
public static function execute(string $class, string $method): void
|
||||
{
|
||||
$key = self::key($class, $method);
|
||||
if (!isset(self::$registry[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defers = self::$registry[$key];
|
||||
unset(self::$registry[$key]);
|
||||
|
||||
DeferExecutor::run($defers);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移除指定类的所有 Defer 注册
|
||||
*/
|
||||
public static function removeClass(string $class): void
|
||||
{
|
||||
$prefix = $class . '::';
|
||||
foreach (array_keys(self::$registry) as $key) {
|
||||
if (str_starts_with($key, $prefix)) {
|
||||
unset(self::$registry[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array{totalKeys: int, totalDefer: int}
|
||||
*/
|
||||
public static function getStats(): array
|
||||
{
|
||||
$totalDefer = 0;
|
||||
foreach (self::$registry as $defers) {
|
||||
$totalDefer += count($defers);
|
||||
}
|
||||
return [
|
||||
'totalKeys' => count(self::$registry),
|
||||
'totalDefer' => $totalDefer,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public static function clear(): void
|
||||
{
|
||||
self::$registry = [];
|
||||
}
|
||||
|
||||
|
||||
private static function key(string $class, string $method): string
|
||||
{
|
||||
return $class . '::' . $method;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,7 +25,8 @@ class ArrayFormat implements IFormat
|
||||
*/
|
||||
public function call($result): ResponseInterface
|
||||
{
|
||||
return $this->response->withBody(new Stream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
||||
$encoded = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
return $this->response->withBody(new Stream($encoded === false ? '[]' : $encoded));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ class MixedFormat implements IFormat
|
||||
return $this->response->withBody(new Stream('[object]'));
|
||||
}
|
||||
if (is_array($result)) {
|
||||
return $this->response->withBody(new Stream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
||||
$encoded = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
return $this->response->withBody(new Stream($encoded === false ? '[]' : $encoded));
|
||||
} else {
|
||||
return $this->response->withBody(new Stream((string)$result));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace Kiri\Router;
|
||||
|
||||
use Closure;
|
||||
use Kiri;
|
||||
use Kiri\Router\Annotate\Defer;
|
||||
use Kiri\Router\Defer\DeferExecutor;
|
||||
use Kiri\Router\Format\IFormat;
|
||||
use Kiri\Router\Format\MixedFormat;
|
||||
use Kiri\Router\Format\NoBody;
|
||||
@@ -32,6 +34,15 @@ class Handler implements RequestHandlerInterface
|
||||
*/
|
||||
protected array $middlewares = [];
|
||||
|
||||
/**
|
||||
* @var Defer[]
|
||||
*/
|
||||
protected array $deferred = [];
|
||||
|
||||
protected ?string $sourceFile = null;
|
||||
|
||||
protected string $sourceKind = 'attribute';
|
||||
|
||||
/**
|
||||
* @param array|Closure $handler
|
||||
* @param array $parameters
|
||||
@@ -124,6 +135,49 @@ class Handler implements RequestHandlerInterface
|
||||
}
|
||||
|
||||
|
||||
public function setSourceFile(?string $sourceFile): void
|
||||
{
|
||||
$this->sourceFile = $sourceFile;
|
||||
}
|
||||
|
||||
|
||||
public function getSourceFile(): ?string
|
||||
{
|
||||
return $this->sourceFile;
|
||||
}
|
||||
|
||||
|
||||
public function setSourceKind(string $sourceKind): void
|
||||
{
|
||||
$this->sourceKind = $sourceKind;
|
||||
}
|
||||
|
||||
|
||||
public function getSourceKind(): string
|
||||
{
|
||||
return $this->sourceKind;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Defer[] $deferred
|
||||
* @return void
|
||||
*/
|
||||
public function setDeferred(array $deferred): void
|
||||
{
|
||||
$this->deferred = $deferred;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Defer[]
|
||||
*/
|
||||
public function getDeferred(): array
|
||||
{
|
||||
return $this->deferred;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
@@ -135,8 +189,26 @@ class Handler implements RequestHandlerInterface
|
||||
|
||||
$data = call_user_func([$controller, $this->handler[1]], ...$this->parameters);
|
||||
|
||||
$this->executeDeferred();
|
||||
|
||||
/** 根据返回类型 */
|
||||
return $this->format->call($data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 异步执行 Defer 回调 — 委托 DeferExecutor 处理协程安全与上下文注入
|
||||
*/
|
||||
private function executeDeferred(): void
|
||||
{
|
||||
if (empty($this->deferred)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defers = $this->deferred;
|
||||
$this->deferred = [];
|
||||
|
||||
DeferExecutor::run($defers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
use PhpParser\Error;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeDumper;
|
||||
use PhpParser\NodeFactory;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
use PhpParser\ParserFactory;
|
||||
use PhpParser\PrettyPrinter\Standard;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class MiddlewareProxyGenerator {
|
||||
private static string $cacheFile = __DIR__ . '/middleware_proxy.php';
|
||||
private static NodeFactory $factory;
|
||||
|
||||
public static function generateProxy(array $middlewares): string {
|
||||
self::$factory = new NodeFactory();
|
||||
|
||||
// 创建类结构(v5.5.0兼容写法)
|
||||
$class = new Node\Stmt\Class_(
|
||||
'MiddlewareProxy',
|
||||
[
|
||||
'implements' => [new Node\Name\FullyQualified(\Psr\Http\Server\MiddlewareInterface::class)],
|
||||
'stmts' => []
|
||||
]
|
||||
);
|
||||
|
||||
// 生成中间件调用链
|
||||
$method = self::createMethodNode($middlewares);
|
||||
$class->stmts[] = $method;
|
||||
|
||||
// 生成代码
|
||||
$printer = new Standard();
|
||||
return $printer->prettyPrintFile([$class]);
|
||||
}
|
||||
|
||||
private static function createMethodNode(array $middlewares): Node\Stmt\Function_ {
|
||||
$method = new Node\Stmt\Function_(
|
||||
'process',
|
||||
[
|
||||
'params' => [
|
||||
new Node\Param(
|
||||
new Node\Expr\Variable('request'),
|
||||
null,
|
||||
new Node\UnionType([new Node\Expr\Variable(\Psr\Http\Message\ServerRequestInterface::class)])
|
||||
),
|
||||
new Node\Param(
|
||||
new Node\Expr\Variable('handler'),
|
||||
null,
|
||||
new Node\UnionType([new Node\Expr\Variable(\Psr\Http\Server\RequestHandlerInterface::class)])
|
||||
)
|
||||
],
|
||||
'returnType' => new Node\Name(ResponseInterface::class),
|
||||
'stmts' => []
|
||||
]
|
||||
);
|
||||
|
||||
// 生成中间件调用链(v5.5.0闭包写法)
|
||||
$chain = [];
|
||||
$count = count($middlewares);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$className = $middlewares[$i]::class;
|
||||
$chain[] = self::createMiddlewareCall($i, $className, $count);
|
||||
}
|
||||
|
||||
$method->stmts = array_reverse($chain);
|
||||
return $method;
|
||||
}
|
||||
|
||||
private static function createMiddlewareCall(int $index, string $className, int $count): Node {
|
||||
$mwVar = new Node\Expr\Variable("mw{$index}");
|
||||
$nextHandler = new Node\Expr\Variable("next");
|
||||
|
||||
// 创建中间件实例(v5.5.0兼容)
|
||||
$instanciate = new Node\Expr\New_($className);
|
||||
|
||||
// 创建闭包表达式(v5.5.0语法)
|
||||
$closure = new Node\Expr\Closure([
|
||||
'static' => true,
|
||||
'params' => [new Node\Param($nextHandler)],
|
||||
'stmts' => [
|
||||
new Node\Stmt\Expression(
|
||||
new Node\Expr\MethodCall(
|
||||
$instanciate,
|
||||
'process',
|
||||
[new Node\Arg($nextHandler), new Node\Arg($mwVar)]
|
||||
)
|
||||
)
|
||||
]
|
||||
]);
|
||||
|
||||
return new Node\Stmt\Expression($closure);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成代理类代码
|
||||
$middlewares = [AuthMiddleware::class, LogMiddleware::class];
|
||||
$proxyCode = MiddlewareProxyGenerator::generateProxy($middlewares);
|
||||
|
||||
// 写入代理类文件
|
||||
file_put_contents(MiddlewareProxyGenerator::$cacheFile, $proxyCode);
|
||||
@@ -34,6 +34,9 @@ class OnRequest implements OnRequestInterface
|
||||
public RouterCollector $router;
|
||||
|
||||
|
||||
public DataGrip $dataGrip;
|
||||
|
||||
|
||||
/**
|
||||
* @var ExceptionHandlerInterface
|
||||
*/
|
||||
@@ -59,6 +62,7 @@ class OnRequest implements OnRequestInterface
|
||||
*/
|
||||
public function __construct(public ResponseInterface $response, DataGrip $dataGrip)
|
||||
{
|
||||
$this->dataGrip = $dataGrip;
|
||||
$this->responseEmitter = $this->response->emmit;
|
||||
$exception = \config('servers.request.exception');
|
||||
if (!in_array(ExceptionHandlerInterface::class, class_implements($exception))) {
|
||||
@@ -82,6 +86,7 @@ class OnRequest implements OnRequestInterface
|
||||
/** @var CQ $PsrRequest */
|
||||
Context::set(ResponseInterface::class, new ConstrictResponse($this->response->contentType));
|
||||
$PsrRequest = Context::set(RequestInterface::class, CQ::builder($request));
|
||||
$this->router = $this->dataGrip->get(ROUTER_TYPE_HTTP);
|
||||
|
||||
CoordinatorManager::utility(Coordinator::WORKER_START)->yield();
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Router;
|
||||
|
||||
class RouteArtifactState
|
||||
{
|
||||
public function store(string $type, array $artifact): void
|
||||
{
|
||||
$payload = [
|
||||
'timestamp' => time(),
|
||||
'type' => $type,
|
||||
'artifact' => $artifact,
|
||||
];
|
||||
|
||||
$directory = dirname($this->getFilePath($type));
|
||||
if (!is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($this->getFilePath($type), json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
public function load(string $type): array
|
||||
{
|
||||
$file = $this->getFilePath($type);
|
||||
if (!file_exists($file)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = json_decode((string)file_get_contents($file), true);
|
||||
if (!is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return is_array($data['artifact'] ?? null) ? $data['artifact'] : [];
|
||||
}
|
||||
|
||||
public function has(string $type): bool
|
||||
{
|
||||
return file_exists($this->getFilePath($type));
|
||||
}
|
||||
|
||||
private function getFilePath(string $type): string
|
||||
{
|
||||
$basePath = realpath($_SERVER['PWD'] ?? APP_PATH ?? getcwd()) ?: ($_SERVER['PWD'] ?? APP_PATH ?? getcwd());
|
||||
$basePath = str_replace('\\', '/', $basePath);
|
||||
$runtimePath = defined('APP_PATH')
|
||||
? rtrim(str_replace('\\', '/', APP_PATH), '/') . '/storage/.kiri-route-artifacts/'
|
||||
: sys_get_temp_dir() . '/kiri-route-artifacts/';
|
||||
|
||||
return $runtimePath . md5($basePath . '::' . $type) . '.json';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Router;
|
||||
|
||||
class RouteEntry
|
||||
{
|
||||
/**
|
||||
* @param array $deferred Array of ['callback' => string|array, 'params' => array]
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $requestMethod,
|
||||
public readonly string $path,
|
||||
public readonly string $class,
|
||||
public readonly string $method,
|
||||
public readonly array $middlewares = [],
|
||||
public readonly ?string $sourceFile = null,
|
||||
public readonly string $sourceKind = 'attribute',
|
||||
public readonly array $deferred = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
+230
-12
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
namespace Kiri\Router;
|
||||
|
||||
use Closure;
|
||||
use Kiri\Di\HotReloadState;
|
||||
use Kiri\Server\Events\OnWorkerStart;
|
||||
use Kiri;
|
||||
use Kiri\Abstracts\CoordinatorManager;
|
||||
use Kiri\Coordinator;
|
||||
use Kiri\Router\RouteArtifactState;
|
||||
use Kiri\Router\Validator\ValidatorMiddleware;
|
||||
use Kiri\Router\Base\Middleware as MiddlewareManager;
|
||||
use Kiri\Router\Constrict\RequestMethod;
|
||||
@@ -35,6 +37,16 @@ class Router
|
||||
*/
|
||||
private static string $type = ROUTER_TYPE_HTTP;
|
||||
|
||||
private static ?string $currentSourceFile = null;
|
||||
|
||||
/**
|
||||
* 标记首次完整扫描是否已完成
|
||||
* Master 进程中完成扫描后设为 true,Worker 通过 fork 继承此标记
|
||||
* Worker 启动时检查此标记,避免重复执行全量 app 目录扫描导致 OOM
|
||||
* @var bool
|
||||
*/
|
||||
private static bool $initialScanDone = false;
|
||||
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
@@ -182,18 +194,87 @@ class Router
|
||||
|
||||
|
||||
/**
|
||||
* 扫描并构建路由表
|
||||
*
|
||||
* Master 进程:执行完整扫描(路由文件加载 + app 目录扫描 + DeferRegistry 注入)
|
||||
* Worker 进程(首次启动):仅加载路由文件注册路由表,跳过全量 app 扫描
|
||||
* Worker 进程(热重载):检测到文件变更时执行完整扫描流程
|
||||
*
|
||||
* 设计原因:
|
||||
* - Master 已完成类加载和字节码编译,Worker 通过 fork 继承全部内存
|
||||
* - Worker 重复执行 opcache_compile_file + invalidateClasses 不产生新信息
|
||||
* - 在应用文件较多时(500+),每个 Worker 的全量扫描会消耗数百 MB 内存导致 OOM
|
||||
*
|
||||
* @throws
|
||||
*/
|
||||
public function scan_build_route(): void
|
||||
{
|
||||
$coordinator = CoordinatorManager::utility(Coordinator::WORKER_START);
|
||||
$container = Kiri::getDi();
|
||||
|
||||
$changedFiles = $container->get(HotReloadState::class)->consume();
|
||||
|
||||
// Worker 首次启动(无变更文件 + Master 已完成扫描):
|
||||
// 重新 include 路由文件(Router::get/post 显式注册) + 基于 Master 扫描清单重建注解路由
|
||||
// 避免 opcache_compile_file,仅用 Reflection 重建路由,内存开销极小
|
||||
if (empty($changedFiles) && self::$initialScanDone) {
|
||||
$container->get(DataGrip::class)->reset(static::$type);
|
||||
$this->read_dir_file(APP_PATH . 'routes');
|
||||
$this->rebuildAnnotationRoutes($container);
|
||||
$this->reset($container);
|
||||
$coordinator->done();
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记首次扫描完成(Master 首次启动或 Worker 热重载时执行到此)
|
||||
self::$initialScanDone = true;
|
||||
|
||||
$container->get(DataGrip::class)->reset(static::$type);
|
||||
|
||||
$scanner = $container->get(Kiri\Di\Scanner::class);
|
||||
$artifactState = $container->get(RouteArtifactState::class);
|
||||
$scanConfig = array_merge(
|
||||
config('servers.reload.scan', []),
|
||||
config('site.scanner', [])
|
||||
);
|
||||
$scanner->setConfig($scanConfig);
|
||||
|
||||
$normalizedAppPath = str_replace('\\', '/', APP_PATH . 'app');
|
||||
$normalizedRoutePath = str_replace('\\', '/', APP_PATH . 'routes');
|
||||
$routeChanged = false;
|
||||
$appChangedFiles = [];
|
||||
|
||||
foreach ($changedFiles as $changedFile) {
|
||||
if (str_starts_with($changedFile, $normalizedRoutePath . '/')) {
|
||||
$routeChanged = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($changedFile, $normalizedAppPath . '/')) {
|
||||
$appChangedFiles[] = $changedFile;
|
||||
}
|
||||
}
|
||||
|
||||
$usedArtifact = false;
|
||||
if (($scanConfig['cache_enabled'] ?? false) && !$routeChanged && $artifactState->has(static::$type)) {
|
||||
$artifact = $artifactState->load(static::$type);
|
||||
$router = $container->get(DataGrip::class)->get(static::$type);
|
||||
$usedArtifact = $router->importArtifact($artifact, $appChangedFiles);
|
||||
}
|
||||
|
||||
// routes 目录中的显式路由文件必须每次重建路由表时重新 include。
|
||||
// route artifact 只加速注解路由,不能替代 routes/*.php 的注册副作用。
|
||||
$this->read_dir_file(APP_PATH . 'routes');
|
||||
|
||||
$container = Kiri::getDi();
|
||||
$scanner = $container->get(Kiri\Di\Scanner::class);
|
||||
if (!$routeChanged && !empty($appChangedFiles) && ($scanConfig['cache_enabled'] ?? false)) {
|
||||
$scanner->scanFiles($appChangedFiles, APP_PATH . 'app/', null, !$usedArtifact);
|
||||
} elseif (!$usedArtifact) {
|
||||
$scanner->scan(APP_PATH . 'app/');
|
||||
} else {
|
||||
$scanner->scanFiles([], APP_PATH . 'app/', null, false);
|
||||
}
|
||||
$this->reset($container);
|
||||
$artifactState->store(static::$type, $container->get(DataGrip::class)->get(static::$type)->exportArtifact());
|
||||
|
||||
$coordinator->done();
|
||||
}
|
||||
@@ -208,19 +289,147 @@ class Router
|
||||
public function reset(ContainerInterface $container): void
|
||||
{
|
||||
$router = $container->get(DataGrip::class)->get(static::$type);
|
||||
foreach ($router->getMethods() as $name => $method) {
|
||||
$middlewares = $method->getMiddlewares();
|
||||
|
||||
foreach ($middlewares as $key => $middleware) {
|
||||
$middlewares[$key] = di($middleware);
|
||||
if ((bool)config('servers.reload.scan.prebuild_http_handlers', false)) {
|
||||
$router->warmHttpHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
$requestHandler = new HttpRequestHandler($middlewares, $method);
|
||||
$validator = MiddlewareManager::getValidator($method->getClass(), $method->getMethod());
|
||||
if (!is_null($validator)) {
|
||||
$requestHandler->withValidatorMiddleware(new ValidatorMiddleware(di(ResponseInterface::class), $method->getClass(), $method->getMethod()));
|
||||
|
||||
/**
|
||||
* 基于 Master 扫描清单重建注解路由(轻量级,无文件 I/O)
|
||||
* 遍历 Scanner manifest 中的所有类,用 Reflection 重新发现 #[Route]/#[Get] 等注解
|
||||
* 避免 Worker 重复执行 opcache_compile_file,但确保注解路由不丢失
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @return void
|
||||
*/
|
||||
private function rebuildAnnotationRoutes(ContainerInterface $container): void
|
||||
{
|
||||
$scanner = $container->get(Kiri\Di\Scanner::class);
|
||||
$scanConfig = array_merge(
|
||||
config('servers.reload.scan', []),
|
||||
config('site.scanner', [])
|
||||
);
|
||||
$scanner->setConfig($scanConfig);
|
||||
|
||||
// 从 manifest 获取 Master 扫描过的类
|
||||
$manifestEntries = $scanner->getManifestClasses();
|
||||
$manifestClasses = [];
|
||||
foreach ($manifestEntries as $entry) {
|
||||
if (is_array($entry) && isset($entry['classes'])) {
|
||||
foreach ($entry['classes'] as $c) {
|
||||
$manifestClasses[$c] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关键:manifest 只包含 Scanner 通过 require_once 新发现的类
|
||||
// 但路由文件加载时 $container->get(Controller) 会触发 autoload 提前加载类
|
||||
// 导致 Scanner 的 require_once 变成 no-op,该类及其注解永久丢失
|
||||
// 因此必须合并 get_declared_classes() 补扫所有已声明的用户空间类
|
||||
$allDeclared = get_declared_classes();
|
||||
foreach ($allDeclared as $class) {
|
||||
$manifestClasses[$class] = true;
|
||||
}
|
||||
|
||||
if (empty($manifestClasses)) {
|
||||
\Kiri::getLogger()->warning('Annotation route rebuild: no classes to process');
|
||||
return;
|
||||
}
|
||||
|
||||
$routeCount = 0;
|
||||
$classCount = 0;
|
||||
$errorCount = 0;
|
||||
$dispatchCount = 0;
|
||||
|
||||
// 只处理用户命名空间下的类,排除框架和 PHP 内置类
|
||||
$userNamespaces = $scanConfig['user_namespaces'] ?? ['App\\'];
|
||||
|
||||
foreach (array_keys($manifestClasses) as $class) {
|
||||
$isUserClass = false;
|
||||
foreach ($userNamespaces as $ns) {
|
||||
if (str_starts_with($class, $ns)) {
|
||||
$isUserClass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$isUserClass) {
|
||||
continue;
|
||||
}
|
||||
if (!class_exists($class)) {
|
||||
continue;
|
||||
}
|
||||
$classCount++;
|
||||
try {
|
||||
$reflect = $container->getReflectionClass($class);
|
||||
if (!$reflect->isInstantiable() || $reflect->isTrait() || $reflect->isEnum() || $reflect->isInterface() || $reflect->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
foreach ($reflect->getMethods() as $method) {
|
||||
if ($method->isStatic() || $method->getDeclaringClass()->getName() !== $class) {
|
||||
continue;
|
||||
}
|
||||
foreach ($method->getAttributes() as $attribute) {
|
||||
$attrName = $attribute->getName();
|
||||
if (!class_exists($attrName)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$instance = $attribute->newInstance();
|
||||
if ($instance instanceof Kiri\Di\Interface\InjectMethodInterface) {
|
||||
$instance->dispatch($class, $method->getName());
|
||||
$dispatchCount++;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$errorCount++;
|
||||
\Kiri::getLogger()->error("Annotation rebuild error [{$class}::{$method->getName()} @ {$attrName}]: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$errorCount++;
|
||||
\Kiri::getLogger()->error("Annotation rebuild class [{$class}]: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$router = $container->get(DataGrip::class)->get(static::$type);
|
||||
$routeCount = count($router->dump());
|
||||
|
||||
\Kiri::getLogger()->info("Annotation route rebuild: {$classCount} user classes processed, {$dispatchCount} annotation routes dispatched, {$routeCount} total routes, {$errorCount} errors");
|
||||
|
||||
// 搜索特定路径的诊断日志
|
||||
$searchPaths = ['/headers'];
|
||||
foreach ($searchPaths as $searchPath) {
|
||||
$found = [];
|
||||
foreach (array_keys($manifestClasses) as $class) {
|
||||
if (!class_exists($class)) continue;
|
||||
try {
|
||||
$reflect = $container->getReflectionClass($class);
|
||||
foreach ($reflect->getMethods() as $method) {
|
||||
foreach ($method->getAttributes() as $attr) {
|
||||
if (in_array($attr->getName(), [
|
||||
\Kiri\Router\Annotate\Get::class,
|
||||
\Kiri\Router\Annotate\Post::class,
|
||||
\Kiri\Router\Annotate\Put::class,
|
||||
\Kiri\Router\Annotate\Delete::class,
|
||||
\Kiri\Router\Annotate\Route::class,
|
||||
])) {
|
||||
$instance = $attr->newInstance();
|
||||
$routePath = $instance->path ?? '';
|
||||
if (str_contains($routePath, 'header')) {
|
||||
$version = $instance->version ?? '';
|
||||
$found[] = "{$class}::{$method->getName()} path={$routePath} version={$version}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {}
|
||||
}
|
||||
if (!empty($found)) {
|
||||
\Kiri::getLogger()->info("Annotation route search '{$searchPath}': " . implode(' | ', $found));
|
||||
} else {
|
||||
\Kiri::getLogger()->warning("Annotation route search '{$searchPath}': NO annotation found in any class");
|
||||
}
|
||||
$router->setHttpHandler($name, $requestHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,10 +462,19 @@ class Router
|
||||
private function resolve_file($files): void
|
||||
{
|
||||
try {
|
||||
static::$currentSourceFile = str_replace('\\', '/', realpath($files) ?: $files);
|
||||
include "$files";
|
||||
} catch (\Throwable $throwable) {
|
||||
\Kiri::getLogger()->json_log($throwable);
|
||||
} finally {
|
||||
static::$currentSourceFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function getCurrentSourceFile(): ?string
|
||||
{
|
||||
return static::$currentSourceFile;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+202
-7
@@ -6,6 +6,8 @@ namespace Kiri\Router;
|
||||
|
||||
|
||||
use Closure;
|
||||
use Kiri\Router\Annotate\Defer;
|
||||
use Kiri\Router\Defer\DeferRegistry;
|
||||
use Kiri\Router\Base\NotFoundController;
|
||||
use Kiri\Router\Constrict\RequestMethod;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
@@ -14,6 +16,7 @@ use Throwable;
|
||||
use Traversable;
|
||||
use Kiri\Router\Base\Middleware;
|
||||
use Kiri\Router\Format\ResponseFormat;
|
||||
use Kiri\Router\Validator\ValidatorMiddleware;
|
||||
|
||||
|
||||
/**
|
||||
@@ -42,7 +45,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
|
||||
|
||||
/**
|
||||
* @var array<string, Handler>
|
||||
* @var array<string, Handler|RouteEntry>
|
||||
*/
|
||||
private array $methods = [];
|
||||
|
||||
@@ -69,7 +72,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
|
||||
|
||||
/**
|
||||
* @return Handler[]
|
||||
* @return array<string, Handler|RouteEntry>
|
||||
*/
|
||||
public function getMethods(): array
|
||||
{
|
||||
@@ -77,6 +80,16 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
}
|
||||
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->_item = [];
|
||||
$this->dump = [];
|
||||
$this->groupTack = [];
|
||||
$this->methods = [];
|
||||
$this->httpHandler = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param HttpRequestHandler $handler
|
||||
@@ -151,7 +164,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
$array[] = [
|
||||
'path' => $path,
|
||||
'method' => $method,
|
||||
'handler' => $controller
|
||||
'handler' => $controller,
|
||||
];
|
||||
}
|
||||
return $array;
|
||||
@@ -171,14 +184,26 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
if (is_string($closure[0])) {
|
||||
$closure[0] = $container->get($closure[0]);
|
||||
}
|
||||
return $interpreter->addRouteByString(... $closure);
|
||||
$handler = $interpreter->addRouteByString(... $closure);
|
||||
$sourceFile = Router::getCurrentSourceFile();
|
||||
if ($sourceFile !== null) {
|
||||
$handler->setSourceFile($sourceFile);
|
||||
$handler->setSourceKind('route_file');
|
||||
}
|
||||
return $handler;
|
||||
}
|
||||
if (!str_contains($closure, '@')) {
|
||||
$closure .= '@';
|
||||
}
|
||||
[$className, $method] = explode('@', $closure);
|
||||
$class = $container->get($this->resetName($className));
|
||||
return $interpreter->addRouteByString($class, $method);
|
||||
$handler = $interpreter->addRouteByString($class, $method);
|
||||
$sourceFile = Router::getCurrentSourceFile();
|
||||
if ($sourceFile !== null) {
|
||||
$handler->setSourceFile($sourceFile);
|
||||
$handler->setSourceKind('route_file');
|
||||
}
|
||||
return $handler;
|
||||
}
|
||||
|
||||
|
||||
@@ -205,8 +230,96 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
*/
|
||||
public function register(string $path, string $method, Handler $handler): void
|
||||
{
|
||||
if ($handler->getSourceFile() === null && $handler->getClass() !== null) {
|
||||
$reflect = \Kiri::getDi()->getReflectionClass($handler->getClass());
|
||||
$handler->setSourceFile($this->normalizePath((string)$reflect->getFileName()));
|
||||
$handler->setSourceKind('attribute');
|
||||
}
|
||||
|
||||
$this->methods[$method . '_' . $path] = $handler;
|
||||
$handler->setMiddlewares($this->registerMiddleware($handler->getClass(), $handler->getMethod()));
|
||||
$handler->setDeferred(DeferRegistry::get($handler->getClass(), $handler->getMethod()));
|
||||
}
|
||||
|
||||
|
||||
public function exportArtifact(): array
|
||||
{
|
||||
$entries = [];
|
||||
$hasClosureRoutes = false;
|
||||
|
||||
foreach ($this->methods as $methodPath => $handler) {
|
||||
if ($handler instanceof Handler && $handler->isClosure()) {
|
||||
$hasClosureRoutes = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
[$requestMethod, $path] = explode('_', $methodPath, 2);
|
||||
$class = $handler instanceof Handler ? $handler->getClass() : $handler->class;
|
||||
$method = $handler instanceof Handler ? $handler->getMethod() : $handler->method;
|
||||
$middlewares = $handler instanceof Handler ? $handler->getMiddlewares() : $handler->middlewares;
|
||||
$sourceFile = $handler instanceof Handler ? $handler->getSourceFile() : $handler->sourceFile;
|
||||
$sourceKind = $handler instanceof Handler ? $handler->getSourceKind() : $handler->sourceKind;
|
||||
$deferred = $handler instanceof Handler ? $this->serializeDeferred($handler->getDeferred()) : ($handler->deferred ?? []);
|
||||
|
||||
$entries[] = [
|
||||
'request_method' => $requestMethod,
|
||||
'path' => $path,
|
||||
'class' => $class,
|
||||
'method' => $method,
|
||||
'middlewares' => $middlewares,
|
||||
'source_file' => $sourceFile,
|
||||
'source_kind' => $sourceKind,
|
||||
'deferred' => $deferred,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'has_closure_routes' => $hasClosureRoutes,
|
||||
'entries' => $entries,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function importArtifact(array $artifact, array $excludeSourceFiles = []): bool
|
||||
{
|
||||
if (($artifact['has_closure_routes'] ?? false) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entries = $artifact['entries'] ?? null;
|
||||
if (!is_array($entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$exclude = array_fill_keys(array_map([$this, 'normalizePath'], $excludeSourceFiles), true);
|
||||
foreach ($entries as $entry) {
|
||||
if (!is_array($entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceFile = $entry['source_file'] ?? null;
|
||||
if (is_string($sourceFile) && isset($exclude[$this->normalizePath($sourceFile)])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$class = $entry['class'] ?? null;
|
||||
$method = $entry['method'] ?? null;
|
||||
$requestMethod = $entry['request_method'] ?? null;
|
||||
$path = $entry['path'] ?? null;
|
||||
|
||||
if (!is_string($class) || !is_string($method) || !is_string($requestMethod) || !is_string($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$middlewares = is_array($entry['middlewares'] ?? null) ? $entry['middlewares'] : [];
|
||||
$sourceFile = is_string($sourceFile) ? $this->normalizePath($sourceFile) : null;
|
||||
$sourceKind = is_string($entry['source_kind'] ?? null) ? $entry['source_kind'] : 'attribute';
|
||||
$deferred = is_array($entry['deferred'] ?? null) ? $entry['deferred'] : [];
|
||||
|
||||
$this->methods[$requestMethod . '_' . $path] = new RouteEntry(requestMethod: $requestMethod, path: $path, class: $class, method: $method, middlewares: $middlewares, sourceFile: $sourceFile, sourceKind: $sourceKind, deferred: $deferred);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -264,8 +377,9 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
/**
|
||||
* @param array $response
|
||||
* @param array $middlewares
|
||||
* @return array
|
||||
* @return Defer[]
|
||||
*/
|
||||
|
||||
private function appendMiddleware(array $response, array $middlewares): array
|
||||
{
|
||||
foreach ($middlewares as $middleware) {
|
||||
@@ -289,7 +403,12 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
*/
|
||||
public function query(string $path, string $method): HttpRequestHandler
|
||||
{
|
||||
return $this->httpHandler[$method . '_' . $path] ?? $this->not_found_handler();
|
||||
$key = $method . '_' . $path;
|
||||
if (!isset($this->httpHandler[$key]) && isset($this->methods[$key])) {
|
||||
$this->httpHandler[$key] = $this->compileHandler($this->methods[$key]);
|
||||
}
|
||||
|
||||
return $this->httpHandler[$key] ?? $this->not_found_handler();
|
||||
}
|
||||
|
||||
|
||||
@@ -309,6 +428,14 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
}
|
||||
|
||||
|
||||
public function warmHttpHandlers(): void
|
||||
{
|
||||
foreach ($this->methods as $name => $method) {
|
||||
$this->httpHandler[$name] = $this->compileHandler($method);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @return string
|
||||
@@ -324,6 +451,74 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Defer[] $deferred
|
||||
* @return array
|
||||
*/
|
||||
private function serializeDeferred(array $deferred): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($deferred as $defer) {
|
||||
$result[] = [
|
||||
'callback' => $defer->callback,
|
||||
'params' => $defer->params,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return Defer[]
|
||||
*/
|
||||
private function deserializeDeferred(array $data): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($data as $item) {
|
||||
if (isset($item['callback'])) {
|
||||
$result[] = new Defer($item['callback'], $item['params'] ?? []);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
$resolved = realpath($path) ?: $path;
|
||||
return str_replace('\\', '/', $resolved);
|
||||
}
|
||||
|
||||
|
||||
private function compileHandler(Handler|RouteEntry $method): HttpRequestHandler
|
||||
{
|
||||
if ($method instanceof RouteEntry) {
|
||||
$controller = \Kiri::getDi()->get($method->class);
|
||||
$handler = di(ControllerInterpreter::class)->addRouteByString($controller, $method->method);
|
||||
$handler->setRequestMethod($method->requestMethod);
|
||||
$handler->setMiddlewares($method->middlewares);
|
||||
$handler->setSourceFile($method->sourceFile);
|
||||
$handler->setSourceKind($method->sourceKind);
|
||||
$handler->setDeferred($this->deserializeDeferred($method->deferred));
|
||||
$method = $handler;
|
||||
}
|
||||
|
||||
$middlewares = $method->getMiddlewares();
|
||||
foreach ($middlewares as $key => $middleware) {
|
||||
$middlewares[$key] = di($middleware);
|
||||
}
|
||||
|
||||
$requestHandler = new HttpRequestHandler($middlewares, $method);
|
||||
$validator = Middleware::getValidator($method->getClass(), $method->getMethod());
|
||||
if ($validator !== null) {
|
||||
$requestHandler->withValidatorMiddleware(new ValidatorMiddleware(di(\Psr\Http\Message\ResponseInterface::class), $method->getClass(), $method->getMethod()));
|
||||
}
|
||||
|
||||
return $requestHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @return bool
|
||||
|
||||
Reference in New Issue
Block a user