Files
kiri-router/src/Router.php
T
2026-06-24 21:02:16 +08:00

481 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
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;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
/**
*
*
* $component->set([
* 'request' => [
* 'middlewares' => []
* ]
* ])
*/
class Router
{
const array METHODS = [RequestMethod::REQUEST_POST, RequestMethod::REQUEST_GET, RequestMethod::REQUEST_OPTIONS, RequestMethod::REQUEST_DELETE, RequestMethod::REQUEST_PUT, RequestMethod::REQUEST_HEAD];
/**
* @var string
*/
private static string $type = ROUTER_TYPE_HTTP;
private static ?string $currentSourceFile = null;
/**
* 标记首次完整扫描是否已完成
* Master 进程中完成扫描后设为 trueWorker 通过 fork 继承此标记
* Worker 启动时检查此标记,避免重复执行全量 app 目录扫描导致 OOM
* @var bool
*/
private static bool $initialScanDone = false;
/**
* @param string $name
* @param Closure $closure
*/
public static function addServer(string $name, Closure $closure): void
{
static::$type = $name;
$closure();
static::$type = ROUTER_TYPE_HTTP;
}
/**
* @param Closure $handler
*/
public static function jsonp(Closure $handler): void
{
static::$type = 'json-rpc';
$handler();
static::$type = ROUTER_TYPE_HTTP;
}
/**
* @param string $route
* @param string $handler
*
* @throws
*/
public static function post(string $route, string $handler): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
$router->addRoute([RequestMethod::REQUEST_POST], $route, $handler);
}
/**
* @param string $route
* @param string $handler
*
* @throws
*/
public static function get(string $route, string $handler): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
$router->addRoute([RequestMethod::REQUEST_GET], $route, $handler);
}
/**
* @param string $route
* @param string $handler
*
* @throws
*/
public static function options(string $route, string $handler): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
$router->addRoute([RequestMethod::REQUEST_OPTIONS], $route, $handler);
}
/**
* @param string $route
* @param string $handler
*
* @throws
*/
public static function any(string $route, string $handler): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
$router->addRoute(self::METHODS, $route, $handler);
}
/**
* @param string $route
* @param string $handler
*
* @throws
*/
public static function delete(string $route, string $handler): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
$router->addRoute([RequestMethod::REQUEST_DELETE], $route, $handler);
}
/**
* @param string $route
* @param string $handler
*
* @throws
*/
public static function head(string $route, string $handler): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
$router->addRoute([RequestMethod::REQUEST_HEAD], $route, $handler);
}
/**
* @param string $route
* @param string $handler
*
* @throws
*/
public static function put(string $route, string $handler): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
$router->addRoute([RequestMethod::REQUEST_PUT], $route, $handler);
}
/**
* @param array|RequestMethod $methods
* @param string $route
* @param string|array $handler
*/
public static function addRoute(array|RequestMethod $methods, string $route, string|array $handler): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
if ($methods instanceof RequestMethod) {
$methods = [$methods];
}
$router->addRoute($methods, $route, $handler);
}
/**
* @param array $config
* @param Closure $closure
*
* @throws
*/
public static function group(array $config, Closure $closure): void
{
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
$router->groupTack[] = $config;
call_user_func($closure);
array_pop($router->groupTack);
}
/**
* 扫描并构建路由表
*
* 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);
}
if (!$usedArtifact) {
$this->read_dir_file(APP_PATH . 'routes');
}
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();
}
/**
* @param ContainerInterface $container
*
* @return void
* @throws
*/
public function reset(ContainerInterface $container): void
{
$router = $container->get(DataGrip::class)->get(static::$type);
if ((bool)config('servers.reload.scan.prebuild_http_handlers', false)) {
$router->warmHttpHandlers();
}
}
/**
* 基于 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
*
* @return void
* @throws
*/
private function read_dir_file($path): void
{
$files = glob($path . '/*');
for ($i = 0; $i < count($files); $i++) {
$file = $files[$i];
if (is_dir($file)) {
$this->read_dir_file($file);
} else {
$this->resolve_file($file);
}
}
}
/**
* @param $files
*
* @throws
*/
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;
}
}