11 Commits

Author SHA1 Message Date
as2252258 2239a34681 eee 2026-06-26 21:57:07 +08:00
as2252258 3daa4021ec eee 2026-06-24 21:15:14 +08:00
as2252258 7455dc8d58 eee 2026-06-24 21:02:16 +08:00
as2252258 5a1aa2d60f eee 2026-06-24 20:56:50 +08:00
as2252258 e40db9d1fb eee 2026-06-24 20:49:06 +08:00
as2252258 b5e0026816 eee 2026-06-24 20:45:37 +08:00
as2252258 69804ea595 eee 2026-06-24 20:37:13 +08:00
as2252258 f099ca8402 eee 2026-06-24 20:17:55 +08:00
as2252258 0087115bdc eee 2026-06-24 20:11:11 +08:00
as2252258 fead1203be eee 2026-06-24 07:26:00 +08:00
as2252258 28973df8b6 eee 2026-06-12 23:57:20 +08:00
13 changed files with 957 additions and 111 deletions
+1 -1
View File
@@ -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
View File
@@ -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"
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Kiri\Router\Annotate;
use Kiri\Di\Interface\InjectMethodInterface;
#[\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 = [])
{
}
/**
* @param string $class
* @param string $method
* @return void
*/
public function dispatch(string $class, string $method): void
{
DeferRegistry::add($class, $method, $this);
}
}
+248
View File
@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace Kiri\Router\Annotate;
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;
/**
* @param string $className
* @param array $construct
* @return object
* @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);
}
/**
* @param string $className
* @return string
* @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]);
}
/**
* @param ReflectionMethod $method
* @return Stmt\ClassMethod
*/
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());
}
$parentCall = new Expr\StaticCall(
new Name('parent'),
$methodName,
$args
);
$stmts = [
new Stmt\Expression(new Expr\Assign(new Expr\Variable('result'), $parentCall)),
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,
]
);
}
/**
* @param \ReflectionParameter $param
* @return Node\Expr
*/
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')),
};
}
/**
* @param mixed $value
* @return Node\Expr
*/
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')),
};
}
/**
* @param string $className
* @return string|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';
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Kiri\Router\Annotate;
class DeferRegistry
{
/**
* @var array<string, Defer[]> "ClassName::method" => Defer[]
*/
private static array $registry = [];
/**
* @param string $class
* @param string $method
* @param Defer $defer
* @return void
*/
public static function add(string $class, string $method, Defer $defer): void
{
$key = self::key($class, $method);
self::$registry[$key][] = $defer;
}
/**
* @param string $class
* @param string $method
* @return Defer[]
*/
public static function get(string $class, string $method): array
{
return self::$registry[self::key($class, $method)] ?? [];
}
/**
* @param string $class
* @return bool
*/
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;
}
/**
* @param string $class
* @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;
}
/**
* @param string $class
* @param string $method
* @return void
*/
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]);
foreach ($defers as $defer) {
try {
$callback = $defer->callback;
$params = $defer->params;
if (is_array($callback)) {
[$cbClass, $cbMethod] = $callback;
$instance = \Kiri::getDi()->get($cbClass);
call_user_func([$instance, $cbMethod], ...$params);
} else {
$instance = \Kiri::getDi()->get($callback);
call_user_func([$instance, '__invoke'], ...$params);
}
} catch (\Throwable $throwable) {
\Kiri::getLogger()->error('Defer callback failed: ' . $throwable->getMessage());
}
}
}
/**
* 移除指定类的所有 Defer 注册,用于热重载时清理失效类
* @param string $class
* @return void
*/
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,
];
}
/**
* @return void
*/
public static function clear(): void
{
self::$registry = [];
}
/**
* @param string $class
* @param string $method
* @return string
*/
private static function key(string $class, string $method): string
{
return $class . '::' . $method;
}
}
+2 -1
View File
@@ -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);
}
+2 -1
View File
@@ -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));
}
+2 -1
View File
@@ -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));
}
+52
View File
@@ -5,6 +5,7 @@ namespace Kiri\Router;
use Closure;
use Kiri;
use Kiri\Router\Annotate\Defer;
use Kiri\Router\Format\IFormat;
use Kiri\Router\Format\MixedFormat;
use Kiri\Router\Format\NoBody;
@@ -32,6 +33,11 @@ class Handler implements RequestHandlerInterface
*/
protected array $middlewares = [];
/**
* @var Defer[]
*/
protected array $deferred = [];
protected ?string $sourceFile = null;
protected string $sourceKind = 'attribute';
@@ -152,6 +158,25 @@ class Handler implements RequestHandlerInterface
}
/**
* @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
@@ -163,8 +188,35 @@ class Handler implements RequestHandlerInterface
$data = call_user_func([$controller, $this->handler[1]], ...$this->parameters);
$this->executeDeferred();
/** 根据返回类型 */
return $this->format->call($data);
}
/**
* @return void
*/
private function executeDeferred(): void
{
foreach ($this->deferred as $defer) {
try {
$callback = $defer->callback;
$params = $defer->params;
if (is_array($callback)) {
[$class, $method] = $callback;
$instance = Kiri::getDi()->get($class);
call_user_func([$instance, $method], ...$params);
} else {
$instance = Kiri::getDi()->get($callback);
call_user_func([$instance, '__invoke'], ...$params);
}
} catch (\Throwable $throwable) {
\Kiri::getLogger()->error('Defer callback failed: ' . $throwable->getMessage());
}
}
}
}
-102
View File
@@ -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);
+4
View File
@@ -6,6 +6,9 @@ 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,
@@ -14,6 +17,7 @@ class RouteEntry
public readonly array $middlewares = [],
public readonly ?string $sourceFile = null,
public readonly string $sourceKind = 'attribute',
public readonly array $deferred = [],
) {
}
}
+178 -2
View File
@@ -39,6 +39,14 @@ class Router
private static ?string $currentSourceFile = null;
/**
* 标记首次完整扫描是否已完成
* Master 进程中完成扫描后设为 trueWorker 通过 fork 继承此标记
* Worker 启动时检查此标记,避免重复执行全量 app 目录扫描导致 OOM
* @var bool
*/
private static bool $initialScanDone = false;
/**
* @param string $name
@@ -186,14 +194,44 @@ 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);
$scanner = $container->get(Kiri\Di\Scanner::class);
$artifactState = $container->get(RouteArtifactState::class);
$scanConfig = array_merge(
config('servers.reload.scan', []),
@@ -201,7 +239,6 @@ class Router
);
$scanner->setConfig($scanConfig);
$changedFiles = $container->get(HotReloadState::class)->consume();
$normalizedAppPath = str_replace('\\', '/', APP_PATH . 'app');
$normalizedRoutePath = str_replace('\\', '/', APP_PATH . 'routes');
$routeChanged = false;
@@ -258,6 +295,145 @@ class Router
}
/**
* 基于 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");
}
}
}
/**
* @param $path
*
+44 -3
View File
@@ -6,6 +6,8 @@ namespace Kiri\Router;
use Closure;
use Kiri\Router\Annotate\Defer;
use Kiri\Router\Annotate\DeferRegistry;
use Kiri\Router\Base\NotFoundController;
use Kiri\Router\Constrict\RequestMethod;
use Psr\Http\Server\MiddlewareInterface;
@@ -236,6 +238,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
$this->methods[$method . '_' . $path] = $handler;
$handler->setMiddlewares($this->registerMiddleware($handler->getClass(), $handler->getMethod()));
$handler->setDeferred(DeferRegistry::get($handler->getClass(), $handler->getMethod()));
}
@@ -256,6 +259,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
$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,
@@ -265,6 +269,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
'middlewares' => $middlewares,
'source_file' => $sourceFile,
'source_kind' => $sourceKind,
'deferred' => $deferred,
];
}
@@ -314,6 +319,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
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'] : [],
);
}
@@ -373,10 +379,11 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
/**
* @param array $response
* @param array $middlewares
* @return array
* @param string $class
* @param string $method
* @return Defer[]
*/
private function appendMiddleware(array $response, array $middlewares): array
{
foreach ($middlewares as $middleware) {
@@ -448,6 +455,39 @@ 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;
@@ -464,6 +504,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
$handler->setMiddlewares($method->middlewares);
$handler->setSourceFile($method->sourceFile);
$handler->setSourceKind($method->sourceKind);
$handler->setDeferred($this->deserializeDeferred($method->deferred));
$method = $handler;
}