Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48298ef1f7 | |||
| d4a1e9c8d7 | |||
| f9ac567bfe | |||
| be7c5da071 | |||
| fec0715c40 | |||
| 2239a34681 | |||
| 3daa4021ec | |||
| 7455dc8d58 | |||
| 5a1aa2d60f | |||
| e40db9d1fb | |||
| b5e0026816 | |||
| 69804ea595 | |||
| f099ca8402 | |||
| 0087115bdc | |||
| fead1203be | |||
| 28973df8b6 | |||
| 4cfd04c988 | |||
| 16b8df159a | |||
| 83f6dc721a | |||
| a2138bdd3b | |||
| d098b293a4 | |||
| f2f2ee408c | |||
| 76d2d2c64e | |||
| 8778cd220f | |||
| 6929bed90a | |||
| 2f08fad1ef | |||
| 91e0fe8940 | |||
| 34051feb87 | |||
| d32fb67650 | |||
| b28b45b15e | |||
| 371e11baa9 | |||
| 5358205fbe | |||
| f4430e2dd0 | |||
| 3a41eaa95c | |||
| 6cb95f4749 | |||
| 299255ce7a | |||
| 51587768cf | |||
| 8165b61f72 | |||
| 0c17482b20 | |||
| 4c9f33a6dc | |||
| 42f945c195 | |||
| a11e6a3eee | |||
| dfa50c9448 | |||
| ad56abe39d | |||
| c148c6135a | |||
| 24cb6e2b5b | |||
| 443df009e1 | |||
| 7b9475a1c9 | |||
| 67bd83933e | |||
| 1f4916f661 | |||
| 44f8bc6535 | |||
| 30cc98c00c | |||
| 9968ea778e | |||
| 870e9b530e | |||
| 669b8ed49f | |||
| d626d5bed7 | |||
| ea3eacdb84 | |||
| 01ad7d7416 | |||
| 58cb9b53db | |||
| c243348e78 | |||
| 05d3110522 | |||
| 1b08ae2be3 | |||
| 45ed584435 | |||
| 5ba0f45528 | |||
| 791267c26f | |||
| 8d53079ba4 | |||
| 2d42ed7090 | |||
| 99af7df9fb | |||
| 6990ef5bcc | |||
| 705e916bda | |||
| 92dd57429d | |||
| 975e7a3cd0 | |||
| 15d54e8ffe | |||
| 7b1767ab5f | |||
| 804b9bd67f | |||
| 153667f0b4 | |||
| 89ac36c227 | |||
| 79ce7142d9 | |||
| 90ee572092 | |||
| 926cbea0b9 | |||
| 3251045a5b | |||
| 5b8163b79e | |||
| be99f4dcbb | |||
| cd79309db3 | |||
| 0c2462feee | |||
| 4c4a21dd7a | |||
| 8623a036ed | |||
| f38942f4f3 | |||
| dc561cec9b | |||
| daa02a6408 | |||
| 34ab8f145c | |||
| ae20755bd7 | |||
| 011e95a3f2 |
@@ -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,81 @@
|
|||||||
# kiri-router
|
# kiri-router
|
||||||
|
|
||||||
|
## Session 使用说明
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Kiri\Router\Session;
|
||||||
|
|
||||||
|
// 存储 Session 数据
|
||||||
|
Session::put('user_id', 123);
|
||||||
|
Session::put('username', 'admin');
|
||||||
|
|
||||||
|
// 或者批量设置
|
||||||
|
Session::put(['user_id' => 123, 'username' => 'admin']);
|
||||||
|
|
||||||
|
// 获取 Session 数据
|
||||||
|
$userId = Session::get('user_id');
|
||||||
|
$username = Session::get('username', 'guest'); // 带默认值
|
||||||
|
|
||||||
|
// 获取所有 Session 数据
|
||||||
|
$all = Session::get(); // 或 Session::all()
|
||||||
|
|
||||||
|
// 检查 Session 中是否存在指定键
|
||||||
|
if (Session::has('user_id')) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除 Session 数据
|
||||||
|
Session::forget('user_id');
|
||||||
|
|
||||||
|
// 清空所有 Session 数据
|
||||||
|
Session::flush();
|
||||||
|
|
||||||
|
// 销毁 Session(包括文件)
|
||||||
|
Session::destroy();
|
||||||
|
|
||||||
|
// 重新生成 Session ID
|
||||||
|
Session::regenerate();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用中间件自动管理 Session
|
||||||
|
|
||||||
|
在配置文件中添加 SessionMiddleware 到中间件列表:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/request.php
|
||||||
|
return [
|
||||||
|
'middlewares' => [
|
||||||
|
\Kiri\Router\Base\SessionMiddleware::class,
|
||||||
|
// 其他中间件...
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
或者使用注解在控制器方法上:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Kiri\Router\Annotate\Middleware;
|
||||||
|
use Kiri\Router\Base\SessionMiddleware;
|
||||||
|
|
||||||
|
class UserController
|
||||||
|
{
|
||||||
|
#[Middleware(SessionMiddleware::class)]
|
||||||
|
public function login()
|
||||||
|
{
|
||||||
|
Session::put('user_id', 123);
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置 Session
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 初始化 Session(可选,有默认值)
|
||||||
|
Session::init(
|
||||||
|
savePath: storage(null, 'session'), // Session 存储路径
|
||||||
|
lifetime: 7200 // Session 生命周期(秒),默认2小时
|
||||||
|
);
|
||||||
|
```
|
||||||
+5
-4
@@ -9,7 +9,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.0",
|
"php": ">=8.5",
|
||||||
"composer-runtime-api": "^2.0",
|
"composer-runtime-api": "^2.0",
|
||||||
"psr/http-server-middleware": "^1.0",
|
"psr/http-server-middleware": "^1.0",
|
||||||
"psr/http-message": "^1.0"
|
"psr/http-message": "^1.0"
|
||||||
@@ -17,8 +17,9 @@
|
|||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Kiri\\Router\\": "./src"
|
"Kiri\\Router\\": "./src"
|
||||||
}
|
},
|
||||||
},
|
"files": [
|
||||||
"require-dev": {
|
"./src/function.php"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,8 @@
|
|||||||
|
<nav style="background: #f0f0f0; padding: 10px;">
|
||||||
|
<ul style="list-style: none; display: flex; gap: 20px; margin: 0; padding: 0;">
|
||||||
|
<li><a href="/">首页</a></li>
|
||||||
|
<li><a href="/about">关于</a></li>
|
||||||
|
<li><a href="/contact">联系</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>@yield('title', 'Kiri Blade 示例')</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>@yield('header', 'Kiri Blade 模板引擎')</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© {{ date('Y') }} Kiri Framework. 使用 Blade 模板引擎构建。</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Blade 模板引擎测试示例
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* php examples/test-blade.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Kiri\Router\Blade\BladeFactory;
|
||||||
|
use Kiri\Router\Blade\BladeHelper;
|
||||||
|
|
||||||
|
// 设置视图路径和缓存路径
|
||||||
|
$viewPath = __DIR__ . '/';
|
||||||
|
$cachePath = __DIR__ . '/cache';
|
||||||
|
|
||||||
|
// 创建 BladeFactory 实例
|
||||||
|
$factory = new BladeFactory($viewPath, $cachePath);
|
||||||
|
BladeHelper::setFactory($factory);
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
$data = [
|
||||||
|
'name' => '张三',
|
||||||
|
'email' => 'zhangsan@example.com',
|
||||||
|
'age' => 28,
|
||||||
|
'skills' => ['PHP', 'JavaScript', 'MySQL', 'NoSql'],
|
||||||
|
'posts' => [
|
||||||
|
[
|
||||||
|
'title' => 'Blade 模板引擎介绍',
|
||||||
|
'content' => '这是一个类似 Laravel Blade 的模板引擎实现。',
|
||||||
|
'date' => '2024-01-15'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => '如何使用 Blade',
|
||||||
|
'content' => 'Blade 提供了简洁优雅的模板语法。',
|
||||||
|
'date' => '2024-01-20'
|
||||||
|
],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 渲染视图
|
||||||
|
try {
|
||||||
|
echo "开始渲染视图...\n\n";
|
||||||
|
$html = $factory->render('user.profile', $data);
|
||||||
|
echo $html;
|
||||||
|
echo "\n\n渲染完成!\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "错误: " . $e->getMessage() . "\n";
|
||||||
|
echo "文件: " . $e->getFile() . "\n";
|
||||||
|
echo "行号: " . $e->getLine() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title')
|
||||||
|
用户资料 - Kiri Blade
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('header')
|
||||||
|
用户资料页面
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h2>用户信息</h2>
|
||||||
|
|
||||||
|
@if(isset($name))
|
||||||
|
<p><strong>姓名:</strong>{{ $name }}</p>
|
||||||
|
@else
|
||||||
|
<p>姓名未设置</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(isset($email))
|
||||||
|
<p><strong>邮箱:</strong>{{ $email }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(isset($age))
|
||||||
|
<p><strong>年龄:</strong>{{ $age }} 岁</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<h3>技能列表</h3>
|
||||||
|
@if(isset($skills) && is_array($skills))
|
||||||
|
<ul>
|
||||||
|
@foreach($skills as $skill)
|
||||||
|
<li>{{ $skill }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p>暂无技能</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<h3>文章列表</h3>
|
||||||
|
@if(isset($posts) && is_array($posts))
|
||||||
|
<div>
|
||||||
|
@foreach($posts as $post)
|
||||||
|
<div style="border: 1px solid #ddd; padding: 10px; margin: 10px 0;">
|
||||||
|
<h4>{{ $post['title'] ?? '无标题' }}</h4>
|
||||||
|
<p>{{ $post['content'] ?? '无内容' }}</p>
|
||||||
|
<small>发布时间:{{ $post['date'] ?? '未知' }}</small>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p>暂无文章</p>
|
||||||
|
@endif
|
||||||
|
@endsection
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ class Middleware implements InjectMethodInterface
|
|||||||
*/
|
*/
|
||||||
public function dispatch(string $class, string $method): void
|
public function dispatch(string $class, string $method): void
|
||||||
{
|
{
|
||||||
MiddlewareManager::set($class, $method, $this->middleware);
|
// MiddlewareManager::set($class, $method, $this->middleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace Kiri\Router\Base;
|
namespace Kiri\Router\Base;
|
||||||
|
|
||||||
use Kiri\Router\Handler;
|
use Kiri\Router\Handler;
|
||||||
|
use Kiri\Router\Validator\ValidatorMiddleware;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
@@ -12,33 +13,50 @@ abstract class AbstractHandler
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
public int $offset = 0;
|
public int $offset = 0;
|
||||||
|
|
||||||
|
|
||||||
/**
|
private ?ValidatorMiddleware $middleware = null;
|
||||||
* @param array $middlewares
|
|
||||||
* @param Handler $handler
|
|
||||||
* @throws
|
|
||||||
*/
|
|
||||||
public function __construct(public array $middlewares, public Handler $handler)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ServerRequestInterface $request
|
|
||||||
* @return ResponseInterface
|
|
||||||
* @throws
|
|
||||||
*/
|
|
||||||
public function execute(ServerRequestInterface $request): ResponseInterface
|
|
||||||
{
|
|
||||||
if (!isset($this->middlewares[$this->offset])) {
|
|
||||||
return $this->handler->handle($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
$middleware = $this->middlewares[$this->offset];
|
/**
|
||||||
$this->offset += 1;
|
* @param array $middlewares
|
||||||
|
* @param Handler $handler
|
||||||
|
*
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public function __construct(public array $middlewares, public Handler $handler)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
return ($middleware instanceof MiddlewareInterface ? $middleware : di($middleware))->process($request, $this);
|
|
||||||
}
|
/**
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public function execute(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
if (!isset($this->middlewares[$this->offset])) {
|
||||||
|
return $this->handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$middleware = $this->middlewares[$this->offset];
|
||||||
|
$this->offset += 1;
|
||||||
|
|
||||||
|
return $middleware->process($request, $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ValidatorMiddleware $middleware
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function withValidatorMiddleware(ValidatorMiddleware $middleware): void
|
||||||
|
{
|
||||||
|
$this->middlewares[] = $middleware;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,21 @@ interface AuthorizationInterface
|
|||||||
public function getUniqueId(): string|int;
|
public function getUniqueId(): string|int;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNickname(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAvatar(): string;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @param int $timeout
|
* @param int $timeout
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function lock(string $key, int $timeout): bool;
|
public function lock(string $key, int $timeout): bool;
|
||||||
|
|||||||
+24
-23
@@ -10,42 +10,43 @@ use Psr\Container\ContainerInterface;
|
|||||||
use Psr\Http\Message\RequestInterface;
|
use Psr\Http\Message\RequestInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Kiri\Di\Inject\Container;
|
||||||
|
use Kiri\Error\StdoutLogger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class WebController
|
* Class WebController
|
||||||
* @package Kiri\Web
|
* @package Kiri\Web
|
||||||
* @property RequestInterface $request
|
|
||||||
* @property ResponseInterface $response
|
|
||||||
* @property ContainerInterface $container
|
|
||||||
* @property Kiri\Error\StdoutLogger $logger
|
|
||||||
*/
|
*/
|
||||||
class Controller extends Kiri\Abstracts\Component
|
class Controller extends Kiri\Abstracts\Component
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Request $request
|
* @var RequestInterface
|
||||||
* @return true
|
|
||||||
*/
|
*/
|
||||||
public function beforeAction(RequestInterface $request): bool
|
#[Container(RequestInterface::class)]
|
||||||
{
|
public RequestInterface $request;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $name
|
* @var ResponseInterface
|
||||||
* @return mixed|ContainerInterface|RequestInterface|ResponseInterface|LoggerInterface
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
*/
|
||||||
public function __get(string $name)
|
#[Container(ResponseInterface::class)]
|
||||||
{
|
public ResponseInterface $response;
|
||||||
return match ($name) {
|
|
||||||
'request' => di(RequestInterface::class),
|
|
||||||
'response' => di(ResponseInterface::class),
|
/**
|
||||||
'container' => di(ContainerInterface::class),
|
* @var LoggerInterface|StdoutLogger
|
||||||
'logger' => di(LoggerInterface::class),
|
*/
|
||||||
default => parent::__get($name)
|
#[Container(LoggerInterface::class)]
|
||||||
}; // TODO: Change the autogenerated stub
|
public LoggerInterface|StdoutLogger $logger;
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ContainerInterface
|
||||||
|
*/
|
||||||
|
#[Container(ContainerInterface::class)]
|
||||||
|
public ContainerInterface $container;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Kiri\Router\Base;
|
namespace Kiri\Router\Base;
|
||||||
|
|
||||||
use Kiri\Di\Inject\Container;
|
use Kiri\Router\Constrict\Stream;
|
||||||
use Kiri\Router\Request;
|
use Kiri\Router\Request;
|
||||||
use Kiri\Router\Response;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
@@ -26,8 +25,12 @@ class CoreMiddleware implements MiddlewareInterface
|
|||||||
*/
|
*/
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
{
|
{
|
||||||
$response = \Kiri::getDi()->get(ResponseInterface::class);
|
$response = response();
|
||||||
$response->withHeaders(['Access-Control-Allow-Headers' => '*', 'Access-Control-Request-Method' => '*', 'Access-Control-Allow-Origin' => '*']);
|
$response->withHeaders([
|
||||||
|
'Access-Control-Allow-Headers' => $request->getHeaderLine('access-control-request-headers'),
|
||||||
|
'Access-Control-Request-Method' => $request->getHeaderLine('access-control-request-method'),
|
||||||
|
'Access-Control-Allow-Origin' => $request->getHeaderLine('origin')
|
||||||
|
]);
|
||||||
return $handler->handle($request);
|
return $handler->handle($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ExceptionHandlerDispatcher implements ExceptionHandlerInterface
|
|||||||
*/
|
*/
|
||||||
public function emit(Throwable $exception, object $response): ResponseInterface
|
public function emit(Throwable $exception, object $response): ResponseInterface
|
||||||
{
|
{
|
||||||
error($exception);
|
\Kiri::getLogger()->json_log($exception);
|
||||||
$response->withContentType(ContentType::HTML)->withBody(new Stream(throwable($exception)));
|
$response->withContentType(ContentType::HTML)->withBody(new Stream(throwable($exception)));
|
||||||
if ($exception->getCode() == 404) {
|
if ($exception->getCode() == 404) {
|
||||||
return $response->withStatus(404);
|
return $response->withStatus(404);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class NotFoundController extends Controller
|
|||||||
public function fail(): ResponseInterface
|
public function fail(): ResponseInterface
|
||||||
{
|
{
|
||||||
if ($this->request->getMethod() == 'OPTIONS') {
|
if ($this->request->getMethod() == 'OPTIONS') {
|
||||||
return $this->response->withStatus(200, "");
|
return $this->response->json([], 200);
|
||||||
} else {
|
} else {
|
||||||
return $this->response->withStatus(404, "not found page.");
|
return $this->response->withStatus(404, "not found page.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Kiri\Router\Base;
|
||||||
|
|
||||||
|
use Kiri\Router\Session;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session 中间件
|
||||||
|
* 自动启动和保存 Session
|
||||||
|
*/
|
||||||
|
class SessionMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @param RequestHandlerInterface $handler
|
||||||
|
* @return ResponseInterface
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
// 启动 Session
|
||||||
|
Session::start($request);
|
||||||
|
|
||||||
|
// 处理请求
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
// 保存 Session
|
||||||
|
Session::save();
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,671 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Kiri\Router\Blade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blade 模板编译器
|
||||||
|
* 将 Blade 语法编译为 PHP 代码
|
||||||
|
*/
|
||||||
|
class BladeCompiler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Blade 指令映射
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
protected array $directives = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义指令处理器
|
||||||
|
* @var array<string, callable>
|
||||||
|
*/
|
||||||
|
protected array $customDirectives = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译后的模板缓存目录
|
||||||
|
*/
|
||||||
|
protected string $cachePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视图文件目录
|
||||||
|
*/
|
||||||
|
protected string $viewPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $viewPath 视图文件路径
|
||||||
|
* @param string $cachePath 编译缓存路径
|
||||||
|
*/
|
||||||
|
public function __construct(string $viewPath, string $cachePath)
|
||||||
|
{
|
||||||
|
$this->viewPath = rtrim($viewPath, '/\\');
|
||||||
|
$this->cachePath = rtrim($cachePath, '/\\');
|
||||||
|
$this->registerDefaultDirectives();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册默认指令
|
||||||
|
*/
|
||||||
|
protected function registerDefaultDirectives(): void
|
||||||
|
{
|
||||||
|
$this->directives = [
|
||||||
|
'if' => 'if',
|
||||||
|
'elseif' => 'elseif',
|
||||||
|
'else' => 'else',
|
||||||
|
'endif' => 'endif',
|
||||||
|
'foreach' => 'foreach',
|
||||||
|
'endforeach' => 'endforeach',
|
||||||
|
'for' => 'for',
|
||||||
|
'endfor' => 'endfor',
|
||||||
|
'while' => 'while',
|
||||||
|
'endwhile' => 'endwhile',
|
||||||
|
'break' => 'break',
|
||||||
|
'continue' => 'continue',
|
||||||
|
'switch' => 'switch',
|
||||||
|
'case' => 'case',
|
||||||
|
'default' => 'default',
|
||||||
|
'endswitch' => 'endswitch',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译模板文件
|
||||||
|
*
|
||||||
|
* @param string $view 视图名称
|
||||||
|
* @return string 编译后的 PHP 文件路径
|
||||||
|
*/
|
||||||
|
public function compile(string $view): string
|
||||||
|
{
|
||||||
|
$viewFile = $this->viewPath . '/' . str_replace('.', '/', $view) . '.blade.php';
|
||||||
|
|
||||||
|
if (!file_exists($viewFile)) {
|
||||||
|
throw new \RuntimeException("视图文件不存在: {$viewFile}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$compiledPath = $this->getCompiledPath($view);
|
||||||
|
$compiledDir = dirname($compiledPath);
|
||||||
|
|
||||||
|
if (!is_dir($compiledDir)) {
|
||||||
|
mkdir($compiledDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果源文件未修改,直接返回缓存的编译文件
|
||||||
|
if (file_exists($compiledPath) && filemtime($viewFile) <= filemtime($compiledPath)) {
|
||||||
|
return $compiledPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($viewFile);
|
||||||
|
$compiled = $this->compileString($content);
|
||||||
|
|
||||||
|
file_put_contents($compiledPath, $compiled);
|
||||||
|
|
||||||
|
return $compiledPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译模板字符串
|
||||||
|
*
|
||||||
|
* @param string $content Blade 模板内容
|
||||||
|
* @param bool $skipLayouts 是否跳过布局编译(避免递归)
|
||||||
|
* @return string 编译后的 PHP 代码
|
||||||
|
*/
|
||||||
|
public function compileString(string $content, bool $skipLayouts = false): string
|
||||||
|
{
|
||||||
|
// 移除注释
|
||||||
|
$content = $this->compileComments($content);
|
||||||
|
|
||||||
|
// 编译 Echo 语句(包括 @json)
|
||||||
|
$content = $this->compileEchos($content);
|
||||||
|
|
||||||
|
// 编译指令
|
||||||
|
$content = $this->compileDirectives($content);
|
||||||
|
|
||||||
|
// 编译条件判断语法糖
|
||||||
|
$content = $this->compileConditionalDirectives($content);
|
||||||
|
|
||||||
|
// 编译布局和继承(如果未跳过)
|
||||||
|
if (!$skipLayouts) {
|
||||||
|
$content = $this->compileLayouts($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译包含和组件
|
||||||
|
$content = $this->compileIncludes($content);
|
||||||
|
|
||||||
|
// 编译 @each 指令
|
||||||
|
$content = $this->compileEach($content);
|
||||||
|
|
||||||
|
// 编译栈和推送
|
||||||
|
$content = $this->compileStacks($content);
|
||||||
|
|
||||||
|
// 编译表单辅助
|
||||||
|
$content = $this->compileFormHelpers($content);
|
||||||
|
|
||||||
|
// 编译原始 PHP
|
||||||
|
$content = $this->compilePhp($content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译注释 {{-- ... --}}
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileComments(string $content): string
|
||||||
|
{
|
||||||
|
return preg_replace('/\{\{--\s*(.*?)\s*--\}\}/s', '', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译 Echo 语句
|
||||||
|
* {{ $var }} => <?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8'); ?>
|
||||||
|
* {!! $var !!} => <?php echo $var; ?>
|
||||||
|
* @json($var) => JSON 编码输出
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileEchos(string $content): string
|
||||||
|
{
|
||||||
|
// 编译转义的 Echo {{ }}
|
||||||
|
$content = preg_replace_callback('/\{\{\s*(.+?)\s*\}\}/', function ($matches) {
|
||||||
|
$expression = trim($matches[1]);
|
||||||
|
return "<?php echo htmlspecialchars({$expression}, ENT_QUOTES, 'UTF-8'); ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// 编译原始 Echo {!! !!}
|
||||||
|
$content = preg_replace_callback('/\{!!\s*(.+?)\s*!!\}/', function ($matches) {
|
||||||
|
$expression = trim($matches[1]);
|
||||||
|
return "<?php echo {$expression}; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// 编译 @json 指令
|
||||||
|
$content = preg_replace_callback('/@json\s*\((.+?)\)/', function ($matches) {
|
||||||
|
$expression = trim($matches[1]);
|
||||||
|
return "<?php echo json_encode({$expression}, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译指令 @if, @foreach 等
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileDirectives(string $content): string
|
||||||
|
{
|
||||||
|
foreach ($this->directives as $directive => $phpDirective) {
|
||||||
|
$pattern = '/@' . $directive . '\s*\((.+?)\)/';
|
||||||
|
$replacement = "<?php {$phpDirective} ($1): ?>";
|
||||||
|
|
||||||
|
$content = preg_replace($pattern, $replacement, $content);
|
||||||
|
|
||||||
|
// 处理结束指令
|
||||||
|
$endPattern = '/@end' . $directive . '/';
|
||||||
|
$endReplacement = "<?php end{$phpDirective}; ?>";
|
||||||
|
$content = preg_replace($endPattern, $endReplacement, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理自定义指令
|
||||||
|
foreach ($this->customDirectives as $directive => $handler) {
|
||||||
|
$pattern = '/@' . $directive . '\s*(?:\((.+?)\))?/';
|
||||||
|
$content = preg_replace_callback($pattern, function ($matches) use ($handler) {
|
||||||
|
$expression = $matches[1] ?? '';
|
||||||
|
return $handler($expression);
|
||||||
|
}, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 @else
|
||||||
|
$content = preg_replace('/@else\b/', '<?php else: ?>', $content);
|
||||||
|
|
||||||
|
// 处理 @elseif
|
||||||
|
$content = preg_replace('/@elseif\s*\((.+?)\)/', '<?php elseif ($1): ?>', $content);
|
||||||
|
|
||||||
|
// 处理 @case
|
||||||
|
$content = preg_replace('/@case\s*\((.+?)\)/', '<?php case $1: ?>', $content);
|
||||||
|
|
||||||
|
// 处理 @default
|
||||||
|
$content = preg_replace('/@default\b/', '<?php default: ?>', $content);
|
||||||
|
|
||||||
|
// 处理 @break 和 @continue
|
||||||
|
$content = preg_replace('/@break\b/', '<?php break; ?>', $content);
|
||||||
|
$content = preg_replace('/@continue\b/', '<?php continue; ?>', $content);
|
||||||
|
|
||||||
|
// 处理 @lang 指令(语言翻译)
|
||||||
|
$content = preg_replace_callback('/@lang\s*\([\'"](.+?)[\'"]\s*(?:,\s*\[(.+?)\])?\)/', function ($matches) {
|
||||||
|
$key = $matches[1];
|
||||||
|
$replace = $matches[2] ?? '[]';
|
||||||
|
return "<?php echo function_exists('__') ? __('{$key}', {$replace}) : '{$key}'; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// 处理 @class 指令(条件类名)
|
||||||
|
$content = preg_replace_callback('/@class\s*\((.+?)\)/', function ($matches) {
|
||||||
|
$conditions = trim($matches[1]);
|
||||||
|
return "<?php echo is_array({$conditions}) ? implode(' ', array_keys(array_filter({$conditions}))) : {$conditions}; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// 处理 @style 指令(条件样式)
|
||||||
|
$content = preg_replace_callback('/@style\s*\((.+?)\)/', function ($matches) {
|
||||||
|
$styles = trim($matches[1]);
|
||||||
|
return "<?php if (is_array({$styles})): echo 'style=\"' . implode('; ', array_map(function(\$k, \$v) { return \$k . ':' . \$v; }, array_keys({$styles}), {$styles})) . '\"'; else: echo 'style=\"' . {$styles} . '\"'; endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译布局和继承
|
||||||
|
* @extends('layout')
|
||||||
|
* @section('content') ... @endsection
|
||||||
|
* @yield('content')
|
||||||
|
* @parent
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileLayouts(string $content): string
|
||||||
|
{
|
||||||
|
// 处理 @extends
|
||||||
|
if (preg_match('/@extends\s*\([\'"](.+?)[\'"]\)/', $content, $matches)) {
|
||||||
|
$layout = $matches[1];
|
||||||
|
$content = preg_replace('/@extends\s*\([\'"](.+?)[\'"]\)/', '', $content);
|
||||||
|
|
||||||
|
// 提取所有 @section
|
||||||
|
$sections = [];
|
||||||
|
$content = preg_replace_callback('/@section\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endsection/s', function ($matches) use (&$sections) {
|
||||||
|
$name = $matches[1];
|
||||||
|
$sectionContent = trim($matches[2]);
|
||||||
|
$sections[$name] = $sectionContent;
|
||||||
|
return '';
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// 获取布局内容
|
||||||
|
$layoutContent = $this->getLayoutContent($layout);
|
||||||
|
|
||||||
|
// 先编译 section 内容
|
||||||
|
$compiledSections = [];
|
||||||
|
foreach ($sections as $name => $sectionContent) {
|
||||||
|
$compiledSections[$name] = $this->compileString($sectionContent, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译布局内容(跳过布局处理以避免递归)
|
||||||
|
$layoutContent = $this->compileString($layoutContent, true);
|
||||||
|
|
||||||
|
// 替换 @yield 为对应的 section 内容
|
||||||
|
foreach ($compiledSections as $name => $sectionContent) {
|
||||||
|
$layoutContent = preg_replace('/@yield\s*\([\'"](?:' . preg_quote($name, '/') . ')[\'"]\)/', $sectionContent, $layoutContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换剩余的 @yield 为空
|
||||||
|
return preg_replace('/@yield\s*\([\'"](.+?)[\'"]\)/', '', $layoutContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 @section ... @endsection (非继承模式,用于组件等)
|
||||||
|
$content = preg_replace_callback('/@section\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endsection/s', function ($matches) {
|
||||||
|
$name = $matches[1];
|
||||||
|
$sectionContent = trim($matches[2]);
|
||||||
|
// 处理 @parent
|
||||||
|
$sectionContent = preg_replace('/@parent/', '', $sectionContent);
|
||||||
|
return "<?php \$__sections['{$name}'] = function() { ?>{$sectionContent}<?php }; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// 处理 @yield(非继承模式)
|
||||||
|
$content = preg_replace_callback('/@yield\s*\([\'"](.+?)[\'"]\s*(?:,\s*[\'"](.+?)[\'"]\s*)?\)/', function ($matches) {
|
||||||
|
$name = $matches[1];
|
||||||
|
$default = $matches[2] ?? '';
|
||||||
|
if ($default) {
|
||||||
|
return "<?php echo isset(\$__sections['{$name}']) ? call_user_func(\$__sections['{$name}']) : '{$default}'; ?>";
|
||||||
|
}
|
||||||
|
return "<?php echo isset(\$__sections['{$name}']) ? call_user_func(\$__sections['{$name}']) : ''; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// 处理 @parent(在 section 中使用)
|
||||||
|
return preg_replace('/@parent/', '', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译包含 @include('view')
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileIncludes(string $content): string
|
||||||
|
{
|
||||||
|
return preg_replace_callback('/@include\s*\([\'"](.+?)[\'"]\s*(?:,\s*\[(.+?)\])?\)/', function ($matches) {
|
||||||
|
$view = $matches[1];
|
||||||
|
$dataStr = $matches[2] ?? '';
|
||||||
|
|
||||||
|
// 解析数据数组
|
||||||
|
if ($dataStr) {
|
||||||
|
// 尝试解析数组字符串,如果失败则使用空数组
|
||||||
|
try {
|
||||||
|
$data = eval("return [{$dataStr}];");
|
||||||
|
$dataCode = var_export($data, true);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Kiri::getLogger()->json_log($e);
|
||||||
|
|
||||||
|
$dataCode = '[]';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$dataCode = '[]';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用静态方法调用,因为我们需要在运行时获取 BladeFactory 实例
|
||||||
|
return "<?php echo \Kiri\Router\Blade\BladeHelper::include('{$view}', {$dataCode}); ?>";
|
||||||
|
}, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译 @each 指令
|
||||||
|
* @each('view', $items, 'item', 'empty')
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileEach(string $content): string
|
||||||
|
{
|
||||||
|
return preg_replace_callback('/@each\s*\([\'"](.+?)[\'"]\s*,\s*(.+?)\s*(?:,\s*[\'"](.+?)[\'"]\s*(?:,\s*[\'"](.+?)[\'"])?)?\)/', function ($matches) {
|
||||||
|
$view = $matches[1];
|
||||||
|
$items = trim($matches[2]);
|
||||||
|
$itemVar = $matches[3] ?? "'item'";
|
||||||
|
$emptyView = $matches[4] ?? null;
|
||||||
|
|
||||||
|
$itemVar = trim($itemVar, "'\"");
|
||||||
|
|
||||||
|
if ($emptyView) {
|
||||||
|
return "<?php if (count({$items}) > 0): foreach ({$items} as \${$itemVar}): echo \Kiri\Router\Blade\BladeHelper::include('{$view}', ['{$itemVar}' => \${$itemVar}]); endforeach; else: echo \Kiri\Router\Blade\BladeHelper::include('{$emptyView}', []); endif; ?>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "<?php foreach ({$items} as \${$itemVar}): echo \Kiri\Router\Blade\BladeHelper::include('{$view}', ['{$itemVar}' => \${$itemVar}]); endforeach; ?>";
|
||||||
|
}, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译条件判断语法糖
|
||||||
|
* @isset, @empty, @auth, @guest, @hasSection, @unless 等
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileConditionalDirectives(string $content): string
|
||||||
|
{
|
||||||
|
// @isset($var) ... @endisset
|
||||||
|
$content = preg_replace_callback('/@isset\s*\((.+?)\)\s*(.*?)\s*@endisset/s', function ($matches) {
|
||||||
|
$var = trim($matches[1]);
|
||||||
|
$body = $matches[2];
|
||||||
|
return "<?php if (isset({$var})): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @empty($var) ... @endempty
|
||||||
|
$content = preg_replace_callback('/@empty\s*\((.+?)\)\s*(.*?)\s*@endempty/s', function ($matches) {
|
||||||
|
$var = trim($matches[1]);
|
||||||
|
$body = $matches[2];
|
||||||
|
return "<?php if (empty({$var})): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @auth ... @endauth
|
||||||
|
$content = preg_replace_callback('/@auth\s*(?:\((.+?)\))?\s*(.*?)\s*@endauth/s', function ($matches) {
|
||||||
|
$guard = $matches[1] ?? '';
|
||||||
|
$body = $matches[2];
|
||||||
|
if ($guard) {
|
||||||
|
return "<?php if (auth('{$guard}')->check()): ?>{$body}<?php endif; ?>";
|
||||||
|
}
|
||||||
|
return "<?php if (function_exists('auth') && auth()->check()): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @guest ... @endguest
|
||||||
|
$content = preg_replace_callback('/@guest\s*(?:\((.+?)\))?\s*(.*?)\s*@endguest/s', function ($matches) {
|
||||||
|
$guard = $matches[1] ?? '';
|
||||||
|
$body = $matches[2];
|
||||||
|
if ($guard) {
|
||||||
|
return "<?php if (!auth('{$guard}')->check()): ?>{$body}<?php endif; ?>";
|
||||||
|
}
|
||||||
|
return "<?php if (!function_exists('auth') || !auth()->check()): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @hasSection('name') ... @endhasSection
|
||||||
|
$content = preg_replace_callback('/@hasSection\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endhasSection/s', function ($matches) {
|
||||||
|
$name = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
return "<?php if (isset(\$__sections['{$name}'])): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @sectionMissing('name') ... @endsectionMissing
|
||||||
|
$content = preg_replace_callback('/@sectionMissing\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endsectionMissing/s', function ($matches) {
|
||||||
|
$name = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
return "<?php if (!isset(\$__sections['{$name}'])): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @unless($condition) ... @endunless
|
||||||
|
$content = preg_replace_callback('/@unless\s*\((.+?)\)\s*(.*?)\s*@endunless/s', function ($matches) {
|
||||||
|
$condition = trim($matches[1]);
|
||||||
|
$body = $matches[2];
|
||||||
|
return "<?php if (!({$condition})): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @can('permission') ... @endcan
|
||||||
|
$content = preg_replace_callback('/@can\s*\([\'"](.+?)[\'"]\s*(?:,\s*(.+?))?\)\s*(.*?)\s*@endcan/s', function ($matches) {
|
||||||
|
$permission = $matches[1];
|
||||||
|
$model = $matches[2] ?? '';
|
||||||
|
$body = $matches[3];
|
||||||
|
if ($model) {
|
||||||
|
return "<?php if (function_exists('can') && can('{$permission}', {$model})): ?>{$body}<?php endif; ?>";
|
||||||
|
}
|
||||||
|
return "<?php if (function_exists('can') && can('{$permission}')): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @cannot('permission') ... @endcannot
|
||||||
|
$content = preg_replace_callback('/@cannot\s*\([\'"](.+?)[\'"]\s*(?:,\s*(.+?))?\)\s*(.*?)\s*@endcannot/s', function ($matches) {
|
||||||
|
$permission = $matches[1];
|
||||||
|
$model = $matches[2] ?? '';
|
||||||
|
$body = $matches[3];
|
||||||
|
if ($model) {
|
||||||
|
return "<?php if (!function_exists('can') || !can('{$permission}', {$model})): ?>{$body}<?php endif; ?>";
|
||||||
|
}
|
||||||
|
return "<?php if (!function_exists('can') || !can('{$permission}')): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @forelse($items as $item) ... @empty ... @endforelse
|
||||||
|
$content = preg_replace_callback('/@forelse\s*\((.+?)\)\s*(.*?)\s*@empty\s*(.*?)\s*@endforelse/s', function ($matches) {
|
||||||
|
$loop = trim($matches[1]);
|
||||||
|
$body = $matches[2];
|
||||||
|
$empty = $matches[3];
|
||||||
|
return "<?php if (count({$loop}) > 0): ?><?php foreach ({$loop}): ?>{$body}<?php endforeach; ?><?php else: ?>{$empty}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @once ... @endonce (只执行一次)
|
||||||
|
$content = preg_replace_callback('/@once\s*(.*?)\s*@endonce/s', function ($matches) {
|
||||||
|
static $onceCounter = 0;
|
||||||
|
$onceCounter++;
|
||||||
|
$hash = md5($matches[0] . $onceCounter);
|
||||||
|
$body = $matches[1];
|
||||||
|
return "<?php if (!isset(\$__once_{$hash})): \$__once_{$hash} = true; ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @error('field') ... @enderror
|
||||||
|
$content = preg_replace_callback('/@error\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@enderror/s', function ($matches) {
|
||||||
|
$field = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
return "<?php if (function_exists('errors') && errors()->has('{$field}')): ?>{$body}<?php endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译栈和推送 @push, @stack, @prepend
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileStacks(string $content): string
|
||||||
|
{
|
||||||
|
// @push('name') ... @endpush
|
||||||
|
$content = preg_replace_callback('/@push\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endpush/s', function ($matches) {
|
||||||
|
$name = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
return "<?php \$__stacks['{$name}'][] = function() { ?>{$body}<?php }; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @prepend('name') ... @endprepend
|
||||||
|
$content = preg_replace_callback('/@prepend\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endprepend/s', function ($matches) {
|
||||||
|
$name = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
return "<?php array_unshift(\$__stacks['{$name}'] ?? [], function() { ?>{$body}<?php }); ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @stack('name')
|
||||||
|
$content = preg_replace_callback('/@stack\s*\([\'"](.+?)[\'"]\)/', function ($matches) {
|
||||||
|
$name = $matches[1];
|
||||||
|
return "<?php if (isset(\$__stacks['{$name}'])): foreach (\$__stacks['{$name}'] as \$__stack): call_user_func(\$__stack); endforeach; endif; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译表单辅助函数
|
||||||
|
* @csrf, @method, @old, @checked, @selected, @disabled
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compileFormHelpers(string $content): string
|
||||||
|
{
|
||||||
|
// @csrf - CSRF token
|
||||||
|
$content = preg_replace('/@csrf/', '<?php echo function_exists("csrf_token") ? csrf_token() : ""; ?>', $content);
|
||||||
|
|
||||||
|
// @method('PUT') - HTTP method spoofing
|
||||||
|
$content = preg_replace_callback('/@method\s*\([\'"](.+?)[\'"]\)/', function ($matches) {
|
||||||
|
$method = strtoupper($matches[1]);
|
||||||
|
return "<input type=\"hidden\" name=\"_method\" value=\"{$method}\">";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @old('field', 'default')
|
||||||
|
$content = preg_replace_callback('/@old\s*\([\'"](.+?)[\'"]\s*(?:,\s*(.+?))?\)/', function ($matches) {
|
||||||
|
$field = $matches[1];
|
||||||
|
$default = $matches[2] ?? "''";
|
||||||
|
return "<?php echo function_exists('old') ? old('{$field}', {$default}) : {$default}; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @checked($condition)
|
||||||
|
$content = preg_replace_callback('/@checked\s*\((.+?)\)/', function ($matches) {
|
||||||
|
$condition = trim($matches[1]);
|
||||||
|
return "<?php echo ({$condition}) ? 'checked' : ''; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @selected($condition)
|
||||||
|
$content = preg_replace_callback('/@selected\s*\((.+?)\)/', function ($matches) {
|
||||||
|
$condition = trim($matches[1]);
|
||||||
|
return "<?php echo ({$condition}) ? 'selected' : ''; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @disabled($condition)
|
||||||
|
$content = preg_replace_callback('/@disabled\s*\((.+?)\)/', function ($matches) {
|
||||||
|
$condition = trim($matches[1]);
|
||||||
|
return "<?php echo ({$condition}) ? 'disabled' : ''; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @readonly($condition)
|
||||||
|
$content = preg_replace_callback('/@readonly\s*\((.+?)\)/', function ($matches) {
|
||||||
|
$condition = trim($matches[1]);
|
||||||
|
return "<?php echo ({$condition}) ? 'readonly' : ''; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
// @required($condition)
|
||||||
|
$content = preg_replace_callback('/@required\s*\((.+?)\)/', function ($matches) {
|
||||||
|
$condition = trim($matches[1]);
|
||||||
|
return "<?php echo ({$condition}) ? 'required' : ''; ?>";
|
||||||
|
}, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译原始 PHP @php ... @endphp
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function compilePhp(string $content): string
|
||||||
|
{
|
||||||
|
return preg_replace('/@php\s*(.*?)\s*@endphp/s', '<?php $1 ?>', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取布局文件内容
|
||||||
|
*
|
||||||
|
* @param string $layout
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getLayoutContent(string $layout): string
|
||||||
|
{
|
||||||
|
$layoutFile = $this->viewPath . '/' . str_replace('.', '/', $layout) . '.blade.php';
|
||||||
|
|
||||||
|
if (!file_exists($layoutFile)) {
|
||||||
|
throw new \RuntimeException("布局文件不存在: {$layoutFile}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_get_contents($layoutFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取编译后的文件路径
|
||||||
|
*
|
||||||
|
* @param string $view
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getCompiledPath(string $view): string
|
||||||
|
{
|
||||||
|
$hash = md5($view);
|
||||||
|
return $this->cachePath . '/' . substr($hash, 0, 2) . '/' . substr($hash, 2, 2) . '/' . $hash . '.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册自定义指令
|
||||||
|
*
|
||||||
|
* @param string $name 指令名称
|
||||||
|
* @param callable $handler 处理函数
|
||||||
|
*/
|
||||||
|
public function directive(string $name, callable $handler): void
|
||||||
|
{
|
||||||
|
$this->customDirectives[$name] = $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除编译缓存
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function clearCache(): void
|
||||||
|
{
|
||||||
|
if (is_dir($this->cachePath)) {
|
||||||
|
$this->deleteDirectory($this->cachePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归删除目录
|
||||||
|
*
|
||||||
|
* @param string $dir
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function deleteDirectory(string $dir): void
|
||||||
|
{
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = array_diff(scandir($dir), ['.', '..']);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$path = $dir . '/' . $file;
|
||||||
|
is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir($dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Kiri\Router\Blade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blade 视图工厂
|
||||||
|
* 管理视图路径、缓存和编译器
|
||||||
|
*/
|
||||||
|
class BladeFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 视图文件路径
|
||||||
|
*/
|
||||||
|
protected string $viewPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译缓存路径
|
||||||
|
*/
|
||||||
|
protected string $cachePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blade 编译器实例
|
||||||
|
*/
|
||||||
|
protected ?BladeCompiler $compiler = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享数据(所有视图可用)
|
||||||
|
*/
|
||||||
|
protected array $shared = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $viewPath 视图文件路径
|
||||||
|
* @param string $cachePath 编译缓存路径
|
||||||
|
*/
|
||||||
|
public function __construct(string $viewPath, string $cachePath)
|
||||||
|
{
|
||||||
|
$this->viewPath = rtrim($viewPath, '/\\');
|
||||||
|
$this->cachePath = rtrim($cachePath, '/\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取编译器实例
|
||||||
|
*
|
||||||
|
* @return BladeCompiler
|
||||||
|
*/
|
||||||
|
public function getCompiler(): BladeCompiler
|
||||||
|
{
|
||||||
|
if ($this->compiler === null) {
|
||||||
|
$this->compiler = new BladeCompiler($this->viewPath, $this->cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->compiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建视图实例
|
||||||
|
*
|
||||||
|
* @param string $view 视图名称
|
||||||
|
* @param array $data 视图数据
|
||||||
|
* @return BladeView
|
||||||
|
*/
|
||||||
|
public function make(string $view, array $data = []): BladeView
|
||||||
|
{
|
||||||
|
// 合并共享数据
|
||||||
|
$data = array_merge($this->shared, $data);
|
||||||
|
|
||||||
|
return new BladeView($view, $data, $this->getCompiler());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染视图
|
||||||
|
*
|
||||||
|
* @param string $view 视图名称
|
||||||
|
* @param array $data 视图数据
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function render(string $view, array $data = []): string
|
||||||
|
{
|
||||||
|
return $this->make($view, $data)->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享数据到所有视图
|
||||||
|
*
|
||||||
|
* @param string|array $key 键名或数据数组
|
||||||
|
* @param mixed $value 值(当 $key 为数组时忽略)
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function share(string|array $key, mixed $value = null): self
|
||||||
|
{
|
||||||
|
if (is_array($key)) {
|
||||||
|
$this->shared = array_merge($this->shared, $key);
|
||||||
|
} else {
|
||||||
|
$this->shared[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册自定义指令
|
||||||
|
*
|
||||||
|
* @param string $name 指令名称
|
||||||
|
* @param callable $handler 处理函数
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function directive(string $name, callable $handler): self
|
||||||
|
{
|
||||||
|
$this->getCompiler()->directive($name, $handler);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除编译缓存
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function clearCache(): void
|
||||||
|
{
|
||||||
|
$this->getCompiler()->clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取视图路径
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getViewPath(): string
|
||||||
|
{
|
||||||
|
return $this->viewPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存路径
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCachePath(): string
|
||||||
|
{
|
||||||
|
return $this->cachePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Kiri\Router\Blade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blade 辅助函数
|
||||||
|
*/
|
||||||
|
class BladeHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 全局 BladeFactory 实例
|
||||||
|
*/
|
||||||
|
protected static ?BladeFactory $factory = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置全局 BladeFactory 实例
|
||||||
|
*
|
||||||
|
* @param BladeFactory $factory
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function setFactory(BladeFactory $factory): void
|
||||||
|
{
|
||||||
|
self::$factory = $factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全局 BladeFactory 实例
|
||||||
|
*
|
||||||
|
* @return BladeFactory
|
||||||
|
*/
|
||||||
|
public static function getFactory(): BladeFactory
|
||||||
|
{
|
||||||
|
if (self::$factory === null) {
|
||||||
|
$viewPath = APP_PATH . 'resources/view';
|
||||||
|
$cachePath = storage(null, 'view/cache');
|
||||||
|
self::$factory = new BladeFactory($viewPath, $cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包含视图
|
||||||
|
*
|
||||||
|
* @param string $view
|
||||||
|
* @param array $data
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function include(string $view, array $data = []): string
|
||||||
|
{
|
||||||
|
return self::getFactory()
|
||||||
|
->render($view, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Kiri\Router\Blade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blade 视图类
|
||||||
|
* 管理视图的渲染和数据传递
|
||||||
|
*/
|
||||||
|
class BladeView
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 视图名称
|
||||||
|
*/
|
||||||
|
protected string $view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视图数据
|
||||||
|
*/
|
||||||
|
protected array $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blade 编译器实例
|
||||||
|
*/
|
||||||
|
protected BladeCompiler $compiler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $view 视图名称
|
||||||
|
* @param array $data 视图数据
|
||||||
|
* @param BladeCompiler $compiler 编译器实例
|
||||||
|
*/
|
||||||
|
public function __construct(string $view, array $data, BladeCompiler $compiler)
|
||||||
|
{
|
||||||
|
$this->view = $view;
|
||||||
|
$this->data = $data;
|
||||||
|
$this->compiler = $compiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染视图
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function render(): string
|
||||||
|
{
|
||||||
|
// 编译视图
|
||||||
|
$compiledPath = $this->compiler->compile($this->view);
|
||||||
|
|
||||||
|
// 提取数据
|
||||||
|
extract($this->data, EXTR_SKIP);
|
||||||
|
|
||||||
|
// 初始化栈和 section 数组
|
||||||
|
$__stacks = [];
|
||||||
|
$__sections = [];
|
||||||
|
|
||||||
|
// 开始输出缓冲
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 包含编译后的 PHP 文件
|
||||||
|
require $compiledPath;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
\Kiri::getLogger()->json_log($throwable);
|
||||||
|
|
||||||
|
throw new \RuntimeException("视图渲染失败: {$this->view}", 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取视图名称
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getView(): string
|
||||||
|
{
|
||||||
|
return $this->view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取视图数据
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getData(): array
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# Kiri Blade 模板引擎
|
||||||
|
|
||||||
|
这是一个类似 Laravel Blade 的模板引擎实现,用于 kiri-router 项目。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 基础功能
|
||||||
|
- ✅ 变量输出:`{{ $variable }}` 和 `{!! $variable !!}`
|
||||||
|
- ✅ JSON 输出:`@json($data)`
|
||||||
|
- ✅ 控制结构:`@if`, `@elseif`, `@else`, `@endif`, `@unless`
|
||||||
|
- ✅ 循环:`@foreach`, `@for`, `@while`, `@forelse` 及其结束指令
|
||||||
|
- ✅ Switch:`@switch`, `@case`, `@default`, `@endswitch`
|
||||||
|
- ✅ 布局继承:`@extends`, `@section`, `@yield`, `@endsection`, `@parent`
|
||||||
|
- ✅ 包含视图:`@include`, `@each`
|
||||||
|
- ✅ 注释:`{{-- comment --}}`
|
||||||
|
- ✅ 原始 PHP:`@php ... @endphp`
|
||||||
|
- ✅ 编译缓存:自动缓存编译后的模板以提高性能
|
||||||
|
|
||||||
|
### 条件判断语法糖
|
||||||
|
- ✅ `@isset` / `@endisset` - 检查变量是否存在
|
||||||
|
- ✅ `@empty` / `@endempty` - 检查变量是否为空
|
||||||
|
- ✅ `@auth` / `@endauth` - 检查用户是否已认证
|
||||||
|
- ✅ `@guest` / `@endguest` - 检查用户是否未认证
|
||||||
|
- ✅ `@hasSection` / `@endhasSection` - 检查 section 是否存在
|
||||||
|
- ✅ `@sectionMissing` / `@endsectionMissing` - 检查 section 是否缺失
|
||||||
|
- ✅ `@can` / `@endcan` - 权限检查
|
||||||
|
- ✅ `@cannot` / `@endcannot` - 反向权限检查
|
||||||
|
|
||||||
|
### 表单辅助
|
||||||
|
- ✅ `@csrf` - CSRF token
|
||||||
|
- ✅ `@method` - HTTP 方法伪装
|
||||||
|
- ✅ `@old` - 旧输入值
|
||||||
|
- ✅ `@checked` - 复选框选中状态
|
||||||
|
- ✅ `@selected` - 下拉框选中状态
|
||||||
|
- ✅ `@disabled` - 禁用状态
|
||||||
|
- ✅ `@readonly` - 只读状态
|
||||||
|
- ✅ `@required` - 必填状态
|
||||||
|
- ✅ `@error` / `@enderror` - 错误信息显示
|
||||||
|
|
||||||
|
### 栈和推送
|
||||||
|
- ✅ `@push` / `@endpush` - 推送到栈
|
||||||
|
- ✅ `@prepend` / `@endprepend` - 前置推送到栈
|
||||||
|
- ✅ `@stack` - 输出栈内容
|
||||||
|
|
||||||
|
### 其他语法糖
|
||||||
|
- ✅ `@once` / `@endonce` - 只执行一次
|
||||||
|
- ✅ `@lang` - 语言翻译
|
||||||
|
- ✅ `@class` - 条件类名
|
||||||
|
- ✅ `@style` - 条件样式
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```php
|
||||||
|
use function Kiri\Router\View;
|
||||||
|
|
||||||
|
// 在控制器中渲染视图
|
||||||
|
return View('user.profile', [
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com'
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 视图文件结构
|
||||||
|
|
||||||
|
视图文件应放在 `resources/view` 目录下,使用 `.blade.php` 扩展名:
|
||||||
|
|
||||||
|
```
|
||||||
|
resources/
|
||||||
|
view/
|
||||||
|
layouts/
|
||||||
|
app.blade.php
|
||||||
|
user/
|
||||||
|
profile.blade.php
|
||||||
|
components/
|
||||||
|
header.blade.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模板语法示例
|
||||||
|
|
||||||
|
#### 1. 变量输出
|
||||||
|
|
||||||
|
```blade
|
||||||
|
<!-- 转义输出 -->
|
||||||
|
<h1>{{ $title }}</h1>
|
||||||
|
<p>{{ $description }}</p>
|
||||||
|
|
||||||
|
<!-- 原始输出(不转义) -->
|
||||||
|
<div>{!! $htmlContent !!}</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 条件语句
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@if($user->isAdmin())
|
||||||
|
<p>管理员</p>
|
||||||
|
@elseif($user->isModerator())
|
||||||
|
<p>版主</p>
|
||||||
|
@else
|
||||||
|
<p>普通用户</p>
|
||||||
|
@endif
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 循环
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@foreach($users as $user)
|
||||||
|
<div>
|
||||||
|
<h3>{{ $user->name }}</h3>
|
||||||
|
<p>{{ $user->email }}</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
@for($i = 0; $i < 10; $i++)
|
||||||
|
<p>Item {{ $i }}</p>
|
||||||
|
@endfor
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 布局继承
|
||||||
|
|
||||||
|
**layouts/app.blade.php:**
|
||||||
|
```blade
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>@yield('title', '默认标题')</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
@include('components.header')
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2024</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**user/profile.blade.php:**
|
||||||
|
```blade
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title')
|
||||||
|
用户资料
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h1>用户资料</h1>
|
||||||
|
<p>姓名:{{ $name }}</p>
|
||||||
|
<p>邮箱:{{ $email }}</p>
|
||||||
|
@endsection
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 包含视图
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@include('components.header')
|
||||||
|
|
||||||
|
@include('components.footer', ['year' => 2024])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. 注释
|
||||||
|
|
||||||
|
```blade
|
||||||
|
{{-- 这是注释,不会输出到 HTML --}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. 原始 PHP
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@php
|
||||||
|
$count = count($items);
|
||||||
|
$total = array_sum($prices);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<p>总数:{{ $count }}</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 自定义指令
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Kiri\Router\Blade\BladeHelper;
|
||||||
|
|
||||||
|
$factory = BladeHelper::getFactory();
|
||||||
|
|
||||||
|
// 注册自定义指令
|
||||||
|
$factory->directive('datetime', function ($expression) {
|
||||||
|
return "<?php echo date('Y-m-d H:i:s', {$expression}); ?>";
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
使用自定义指令:
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@datetime(time())
|
||||||
|
```
|
||||||
|
|
||||||
|
### 共享数据
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Kiri\Router\Blade\BladeHelper;
|
||||||
|
|
||||||
|
$factory = BladeHelper::getFactory();
|
||||||
|
|
||||||
|
// 共享数据到所有视图
|
||||||
|
$factory->share('siteName', 'My Website');
|
||||||
|
$factory->share([
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'author' => 'Kiri Team'
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 清除缓存
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Kiri\Router\Blade\BladeHelper;
|
||||||
|
|
||||||
|
$factory = BladeHelper::getFactory();
|
||||||
|
$factory->clearCache();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
视图路径和缓存路径可以通过常量配置:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 视图路径
|
||||||
|
define('APP_PATH', __DIR__ . '/');
|
||||||
|
|
||||||
|
// 缓存路径(可选)
|
||||||
|
define('RUNTIME_PATH', __DIR__ . '/runtime');
|
||||||
|
```
|
||||||
|
|
||||||
|
默认情况下:
|
||||||
|
- 视图路径:`APP_PATH . 'resources/view'`
|
||||||
|
- 缓存路径:`RUNTIME_PATH . '/blade'` 或系统临时目录
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 模板文件必须使用 `.blade.php` 扩展名
|
||||||
|
2. 视图路径使用点号分隔,如 `user.profile` 对应 `user/profile.blade.php`
|
||||||
|
3. 编译后的模板会自动缓存,修改源文件后会自动重新编译
|
||||||
|
4. 建议在生产环境中定期清理缓存目录
|
||||||
|
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
# Blade 语法糖参考手册
|
||||||
|
|
||||||
|
本文档列出了所有可用的 Blade 语法糖和指令。
|
||||||
|
|
||||||
|
## 变量输出
|
||||||
|
|
||||||
|
### 转义输出
|
||||||
|
```blade
|
||||||
|
{{ $variable }}
|
||||||
|
{{ $user->name }}
|
||||||
|
{{ $array['key'] }}
|
||||||
|
```
|
||||||
|
自动转义 HTML 特殊字符,防止 XSS 攻击。
|
||||||
|
|
||||||
|
### 原始输出
|
||||||
|
```blade
|
||||||
|
{!! $htmlContent !!}
|
||||||
|
```
|
||||||
|
不转义,直接输出原始 HTML(谨慎使用)。
|
||||||
|
|
||||||
|
### JSON 输出
|
||||||
|
```blade
|
||||||
|
@json($data)
|
||||||
|
```
|
||||||
|
将数据编码为 JSON 字符串输出。
|
||||||
|
|
||||||
|
## 控制结构
|
||||||
|
|
||||||
|
### 条件判断
|
||||||
|
|
||||||
|
#### @if / @elseif / @else / @endif
|
||||||
|
```blade
|
||||||
|
@if($user->isAdmin())
|
||||||
|
<p>管理员</p>
|
||||||
|
@elseif($user->isModerator())
|
||||||
|
<p>版主</p>
|
||||||
|
@else
|
||||||
|
<p>普通用户</p>
|
||||||
|
@endif
|
||||||
|
```
|
||||||
|
|
||||||
|
#### @unless / @endunless
|
||||||
|
```blade
|
||||||
|
@unless($user->isBanned())
|
||||||
|
<p>用户正常</p>
|
||||||
|
@endunless
|
||||||
|
```
|
||||||
|
等同于 `@if(!condition)`
|
||||||
|
|
||||||
|
#### @isset / @endisset
|
||||||
|
```blade
|
||||||
|
@isset($variable)
|
||||||
|
<p>{{ $variable }}</p>
|
||||||
|
@endisset
|
||||||
|
```
|
||||||
|
|
||||||
|
#### @empty / @endempty
|
||||||
|
```blade
|
||||||
|
@empty($items)
|
||||||
|
<p>列表为空</p>
|
||||||
|
@endempty
|
||||||
|
```
|
||||||
|
|
||||||
|
### 循环
|
||||||
|
|
||||||
|
#### @foreach / @endforeach
|
||||||
|
```blade
|
||||||
|
@foreach($users as $user)
|
||||||
|
<p>{{ $user->name }}</p>
|
||||||
|
@endforeach
|
||||||
|
```
|
||||||
|
|
||||||
|
#### @for / @endfor
|
||||||
|
```blade
|
||||||
|
@for($i = 0; $i < 10; $i++)
|
||||||
|
<p>Item {{ $i }}</p>
|
||||||
|
@endfor
|
||||||
|
```
|
||||||
|
|
||||||
|
#### @while / @endwhile
|
||||||
|
```blade
|
||||||
|
@while($condition)
|
||||||
|
<p>循环内容</p>
|
||||||
|
@endwhile
|
||||||
|
```
|
||||||
|
|
||||||
|
#### @forelse / @empty / @endforelse
|
||||||
|
```blade
|
||||||
|
@forelse($items as $item)
|
||||||
|
<p>{{ $item->name }}</p>
|
||||||
|
@empty
|
||||||
|
<p>没有项目</p>
|
||||||
|
@endforelse
|
||||||
|
```
|
||||||
|
|
||||||
|
#### @break 和 @continue
|
||||||
|
```blade
|
||||||
|
@foreach($items as $item)
|
||||||
|
@if($item->hidden)
|
||||||
|
@continue
|
||||||
|
@endif
|
||||||
|
<p>{{ $item->name }}</p>
|
||||||
|
@if($item->id > 100)
|
||||||
|
@break
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch 语句
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@switch($status)
|
||||||
|
@case(1)
|
||||||
|
<p>待处理</p>
|
||||||
|
@break
|
||||||
|
@case(2)
|
||||||
|
<p>处理中</p>
|
||||||
|
@break
|
||||||
|
@default
|
||||||
|
<p>未知状态</p>
|
||||||
|
@endswitch
|
||||||
|
```
|
||||||
|
|
||||||
|
## 布局和继承
|
||||||
|
|
||||||
|
### @extends
|
||||||
|
```blade
|
||||||
|
@extends('layouts.app')
|
||||||
|
```
|
||||||
|
|
||||||
|
### @section / @endsection
|
||||||
|
```blade
|
||||||
|
@section('content')
|
||||||
|
<h1>页面内容</h1>
|
||||||
|
@endsection
|
||||||
|
```
|
||||||
|
|
||||||
|
### @yield
|
||||||
|
```blade
|
||||||
|
@yield('content')
|
||||||
|
@yield('title', '默认标题')
|
||||||
|
```
|
||||||
|
|
||||||
|
### @parent
|
||||||
|
在子视图的 section 中使用,输出父布局中对应 section 的内容。
|
||||||
|
|
||||||
|
### @hasSection / @endhasSection
|
||||||
|
```blade
|
||||||
|
@hasSection('sidebar')
|
||||||
|
@yield('sidebar')
|
||||||
|
@endhasSection
|
||||||
|
```
|
||||||
|
|
||||||
|
### @sectionMissing / @endsectionMissing
|
||||||
|
```blade
|
||||||
|
@sectionMissing('sidebar')
|
||||||
|
<p>没有侧边栏</p>
|
||||||
|
@endsectionMissing
|
||||||
|
```
|
||||||
|
|
||||||
|
## 包含和组件
|
||||||
|
|
||||||
|
### @include
|
||||||
|
```blade
|
||||||
|
@include('components.header')
|
||||||
|
@include('components.footer', ['year' => 2024])
|
||||||
|
```
|
||||||
|
|
||||||
|
### @each
|
||||||
|
```blade
|
||||||
|
@each('components.item', $items, 'item')
|
||||||
|
@each('components.item', $items, 'item', 'components.empty')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 栈和推送
|
||||||
|
|
||||||
|
### @push / @endpush
|
||||||
|
```blade
|
||||||
|
@push('scripts')
|
||||||
|
<script src="/js/custom.js"></script>
|
||||||
|
@endpush
|
||||||
|
```
|
||||||
|
|
||||||
|
### @prepend / @endprepend
|
||||||
|
```blade
|
||||||
|
@prepend('scripts')
|
||||||
|
<script src="/js/jquery.js"></script>
|
||||||
|
@endprepend
|
||||||
|
```
|
||||||
|
|
||||||
|
### @stack
|
||||||
|
```blade
|
||||||
|
@stack('scripts')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 表单辅助
|
||||||
|
|
||||||
|
### @csrf
|
||||||
|
```blade
|
||||||
|
<form method="POST">
|
||||||
|
@csrf
|
||||||
|
...
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
### @method
|
||||||
|
```blade
|
||||||
|
<form method="POST">
|
||||||
|
@method('PUT')
|
||||||
|
...
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
### @old
|
||||||
|
```blade
|
||||||
|
<input type="text" name="name" value="@old('name', '默认值')">
|
||||||
|
```
|
||||||
|
|
||||||
|
### @checked
|
||||||
|
```blade
|
||||||
|
<input type="checkbox" name="active" @checked($user->isActive())>
|
||||||
|
```
|
||||||
|
|
||||||
|
### @selected
|
||||||
|
```blade
|
||||||
|
<select name="status">
|
||||||
|
<option value="1" @selected($status == 1)>启用</option>
|
||||||
|
<option value="0" @selected($status == 0)>禁用</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
### @disabled
|
||||||
|
```blade
|
||||||
|
<input type="text" @disabled($readonly)>
|
||||||
|
```
|
||||||
|
|
||||||
|
### @readonly
|
||||||
|
```blade
|
||||||
|
<input type="text" @readonly($readonly)>
|
||||||
|
```
|
||||||
|
|
||||||
|
### @required
|
||||||
|
```blade
|
||||||
|
<input type="text" @required($isRequired)>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 权限和认证
|
||||||
|
|
||||||
|
### @auth / @endauth
|
||||||
|
```blade
|
||||||
|
@auth
|
||||||
|
<p>欢迎,{{ auth()->user()->name }}</p>
|
||||||
|
@endauth
|
||||||
|
|
||||||
|
@auth('admin')
|
||||||
|
<p>管理员面板</p>
|
||||||
|
@endauth
|
||||||
|
```
|
||||||
|
|
||||||
|
### @guest / @endguest
|
||||||
|
```blade
|
||||||
|
@guest
|
||||||
|
<a href="/login">登录</a>
|
||||||
|
@endguest
|
||||||
|
```
|
||||||
|
|
||||||
|
### @can / @endcan
|
||||||
|
```blade
|
||||||
|
@can('edit', $post)
|
||||||
|
<a href="/posts/{{ $post->id }}/edit">编辑</a>
|
||||||
|
@endcan
|
||||||
|
```
|
||||||
|
|
||||||
|
### @cannot / @endcannot
|
||||||
|
```blade
|
||||||
|
@cannot('edit', $post)
|
||||||
|
<p>您没有编辑权限</p>
|
||||||
|
@endcannot
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### @error / @enderror
|
||||||
|
```blade
|
||||||
|
@error('email')
|
||||||
|
<p class="error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
```
|
||||||
|
|
||||||
|
## 其他语法糖
|
||||||
|
|
||||||
|
### @once / @endonce
|
||||||
|
```blade
|
||||||
|
@once
|
||||||
|
<script src="/js/unique.js"></script>
|
||||||
|
@endonce
|
||||||
|
```
|
||||||
|
确保内容只输出一次。
|
||||||
|
|
||||||
|
### @lang
|
||||||
|
```blade
|
||||||
|
@lang('messages.welcome')
|
||||||
|
@lang('messages.greeting', ['name' => $user->name])
|
||||||
|
```
|
||||||
|
|
||||||
|
### @class
|
||||||
|
```blade
|
||||||
|
<div @class(['active' => $isActive, 'disabled' => $isDisabled])>
|
||||||
|
```
|
||||||
|
|
||||||
|
### @style
|
||||||
|
```blade
|
||||||
|
<div @style(['color' => 'red', 'font-size' => '14px'])>
|
||||||
|
```
|
||||||
|
|
||||||
|
### @php / @endphp
|
||||||
|
```blade
|
||||||
|
@php
|
||||||
|
$count = count($items);
|
||||||
|
$total = array_sum($prices);
|
||||||
|
@endphp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注释
|
||||||
|
```blade
|
||||||
|
{{-- 这是注释,不会输出到 HTML --}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 完整的页面模板
|
||||||
|
```blade
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '用户资料')
|
||||||
|
|
||||||
|
@push('styles')
|
||||||
|
<link rel="stylesheet" href="/css/profile.css">
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
@auth
|
||||||
|
<h1>欢迎,{{ auth()->user()->name }}</h1>
|
||||||
|
|
||||||
|
@if($user->isAdmin())
|
||||||
|
<p class="badge">管理员</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="/profile">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<input type="text" name="name" value="@old('name', $user->name)">
|
||||||
|
@error('name')
|
||||||
|
<p class="error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
|
||||||
|
<button type="submit" @disabled($readonly)>保存</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>文章列表</h2>
|
||||||
|
@forelse($posts as $post)
|
||||||
|
<article>
|
||||||
|
<h3>{{ $post->title }}</h3>
|
||||||
|
<p>{{ $post->content }}</p>
|
||||||
|
</article>
|
||||||
|
@empty
|
||||||
|
<p>暂无文章</p>
|
||||||
|
@endforelse
|
||||||
|
@else
|
||||||
|
<p>请先登录</p>
|
||||||
|
@endauth
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script src="/js/profile.js"></script>
|
||||||
|
@endpush
|
||||||
|
```
|
||||||
|
|
||||||
|
### 布局文件
|
||||||
|
```blade
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>@yield('title', '默认标题')</title>
|
||||||
|
@stack('styles')
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
@include('components.header')
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
@include('components.footer')
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
@stack('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
+545
-536
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ use Kiri\Router\ContentType;
|
|||||||
use Kiri\Router\StreamResponse;
|
use Kiri\Router\StreamResponse;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
|
||||||
class ConstrictResponse extends Message implements ResponseInterface
|
class ConstrictResponse extends Message implements ResponseInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -30,6 +31,25 @@ class ConstrictResponse extends Message implements ResponseInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $url
|
||||||
|
* @param array $params
|
||||||
|
* @param int $statusCode
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public function redirectTo(string $url, array $params = [], int $statusCode = 302): static
|
||||||
|
{
|
||||||
|
if (!empty($params)) {
|
||||||
|
$url .= '?' . http_build_query($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->withHeader('Location', $url);
|
||||||
|
$this->withStatus($statusCode);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ContentType $contentType
|
* @param ContentType $contentType
|
||||||
* @return $this
|
* @return $this
|
||||||
@@ -77,7 +97,8 @@ class ConstrictResponse extends Message implements ResponseInterface
|
|||||||
*/
|
*/
|
||||||
public function json(array $content, int $statusCode = 200): static
|
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);
|
return $this->withContentType(ContentType::JSON)->withStatus($statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-50
@@ -23,7 +23,7 @@ class Message extends Component implements MessageInterface
|
|||||||
private array $headers = [];
|
private array $headers = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var StreamInterface
|
* @var StreamInterface
|
||||||
*/
|
*/
|
||||||
public StreamInterface $stream;
|
public StreamInterface $stream;
|
||||||
@@ -31,7 +31,13 @@ class Message extends Component implements MessageInterface
|
|||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private array $cookieParams = [];
|
public array $cookieParams = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public array $data = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,24 +51,24 @@ class Message extends Component implements MessageInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve cookies.
|
* @return array
|
||||||
*
|
*/
|
||||||
* Retrieves cookies sent by the client to the server.
|
public function getHeaders(): array
|
||||||
*
|
{
|
||||||
* The data MUST be compatible with the structure of the $_COOKIE
|
return $this->headers;
|
||||||
* superglobal.
|
}
|
||||||
*
|
|
||||||
* @return array
|
/**
|
||||||
*/
|
* @return array
|
||||||
public function getCookieParams(): array
|
*/
|
||||||
{
|
public function getCookieParams(): array
|
||||||
// TODO: Implement getCookieParams() method.
|
{
|
||||||
return $this->cookieParams;
|
return $this->cookieParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance with the specified cookies.
|
* Return an instance with the specified cookies.
|
||||||
*
|
*
|
||||||
* The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
|
* The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
|
||||||
@@ -120,38 +126,7 @@ class Message extends Component implements MessageInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all message header values.
|
|
||||||
*
|
|
||||||
* The keys represent the header name as it will be sent over the wire, and
|
|
||||||
* each value is an array of strings associated with the header.
|
|
||||||
*
|
|
||||||
* // Represent the headers as a string
|
|
||||||
* foreach ($message->getHeaders() as $name => $values) {
|
|
||||||
* echo $name . ": " . implode(", ", $values);
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // Emit headers iteratively:
|
|
||||||
* foreach ($message->getHeaders() as $name => $values) {
|
|
||||||
* foreach ($values as $value) {
|
|
||||||
* header(sprintf('%s: %s', $name, $value), false);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
|
||||||
* exact case in which headers were originally specified.
|
|
||||||
*
|
|
||||||
* @return string[][] Returns an associative array of the message's headers. Each
|
|
||||||
* key MUST be a header name, and each value MUST be an array of strings
|
|
||||||
* for that header.
|
|
||||||
*/
|
|
||||||
public function getHeaders(): array
|
|
||||||
{
|
|
||||||
// TODO: Implement getHeaders() method.
|
|
||||||
return $this->headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a header exists by the given case-insensitive name.
|
* Checks if a header exists by the given case-insensitive name.
|
||||||
*
|
*
|
||||||
* @param string $name Case-insensitive header field name.
|
* @param string $name Case-insensitive header field name.
|
||||||
|
|||||||
+644
-643
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,14 +9,25 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
class ArrayFormat implements IFormat
|
class ArrayFormat implements IFormat
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $result
|
/**
|
||||||
* @return ResponseInterface
|
* @param ResponseInterface $response
|
||||||
*/
|
*/
|
||||||
public function call($result): ResponseInterface
|
public function __construct(public ResponseInterface $response)
|
||||||
{
|
{
|
||||||
return di(ResponseInterface::class)->withBody(new Stream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $result
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function call($result): ResponseInterface
|
||||||
|
{
|
||||||
|
$encoded = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||||
|
return $this->response->withBody(new Stream($encoded === false ? '[]' : $encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,16 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
class MixedFormat implements IFormat
|
class MixedFormat implements IFormat
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
|
||||||
|
/**
|
||||||
|
* @param ResponseInterface $response
|
||||||
|
*/
|
||||||
|
public function __construct(public ResponseInterface $response)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
* @param mixed $result
|
* @param mixed $result
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
*/
|
*/
|
||||||
@@ -18,14 +27,14 @@ class MixedFormat implements IFormat
|
|||||||
if ($result instanceof ResponseInterface) {
|
if ($result instanceof ResponseInterface) {
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
$response = Kiri::getDi()->get(ResponseInterface::class);
|
|
||||||
if (is_object($result)) {
|
if (is_object($result)) {
|
||||||
return $response->withBody(new Stream('[object]'));
|
return $this->response->withBody(new Stream('[object]'));
|
||||||
}
|
}
|
||||||
if (is_array($result)) {
|
if (is_array($result)) {
|
||||||
return $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 {
|
} else {
|
||||||
return $response->withBody(new Stream((string)$result));
|
return $this->response->withBody(new Stream((string)$result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-23
@@ -9,27 +9,36 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
class NoBody implements IFormat
|
class NoBody implements IFormat
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $result
|
/**
|
||||||
* @return ResponseInterface
|
* @param ResponseInterface $response
|
||||||
*/
|
*/
|
||||||
public function call($result): ResponseInterface
|
public function __construct(public ResponseInterface $response)
|
||||||
{
|
{
|
||||||
// TODO: Implement call() method.
|
}
|
||||||
$response = Kiri::getDi()->get(ResponseInterface::class);
|
|
||||||
if (request()->getMethod() === 'HEAD') {
|
|
||||||
return $response->withBody(new Stream());
|
/**
|
||||||
}
|
* @param $result
|
||||||
if ($result instanceof ResponseInterface) {
|
*
|
||||||
return $result;
|
* @return ResponseInterface
|
||||||
}
|
*/
|
||||||
if (is_object($result)) {
|
public function call($result): ResponseInterface
|
||||||
return $response->withBody(new Stream('[object]'));
|
{
|
||||||
}
|
// TODO: Implement call() method.
|
||||||
if (is_array($result)) {
|
if (request()->getMethod() === 'HEAD') {
|
||||||
return $response->withBody(new Stream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
return $this->response->withBody(new Stream());
|
||||||
} else {
|
}
|
||||||
return $response->withBody(new Stream((string)$result));
|
if ($result instanceof ResponseInterface) {
|
||||||
}
|
return $result;
|
||||||
}
|
}
|
||||||
|
if (is_object($result)) {
|
||||||
|
return $this->response->withBody(new Stream('[object]'));
|
||||||
|
}
|
||||||
|
if (is_array($result)) {
|
||||||
|
return $this->response->withBody(new Stream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
||||||
|
} else {
|
||||||
|
return $this->response->withBody(new Stream((string)$result));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,13 +9,18 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
class OtherFormat implements IFormat
|
class OtherFormat implements IFormat
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
|
||||||
|
public function __construct(public ResponseInterface $response)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* @param mixed $result
|
* @param mixed $result
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
*/
|
*/
|
||||||
public function call(mixed $result): ResponseInterface
|
public function call(mixed $result): ResponseInterface
|
||||||
{
|
{
|
||||||
return di(ResponseInterface::class)->withBody(new Stream($result));
|
return $this->response->withBody(new Stream($result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,23 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
class VoidFormat implements IFormat
|
class VoidFormat implements IFormat
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $result
|
/**
|
||||||
* @return ResponseInterface
|
* @param ResponseInterface $response
|
||||||
*/
|
*/
|
||||||
public function call($result): ResponseInterface
|
public function __construct(public ResponseInterface $response)
|
||||||
{
|
{
|
||||||
// TODO: Implement call() method.
|
}
|
||||||
return di(ResponseInterface::class);
|
|
||||||
}
|
/**
|
||||||
|
* @param $result
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function call($result): ResponseInterface
|
||||||
|
{
|
||||||
|
// TODO: Implement call() method.
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+96
-1
@@ -5,6 +5,8 @@ namespace Kiri\Router;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Kiri;
|
use Kiri;
|
||||||
|
use Kiri\Router\Annotate\Defer;
|
||||||
|
use Kiri\Router\Defer\DeferExecutor;
|
||||||
use Kiri\Router\Format\IFormat;
|
use Kiri\Router\Format\IFormat;
|
||||||
use Kiri\Router\Format\MixedFormat;
|
use Kiri\Router\Format\MixedFormat;
|
||||||
use Kiri\Router\Format\NoBody;
|
use Kiri\Router\Format\NoBody;
|
||||||
@@ -27,6 +29,20 @@ class Handler implements RequestHandlerInterface
|
|||||||
protected array $methods = [];
|
protected array $methods = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $middlewares = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Defer[]
|
||||||
|
*/
|
||||||
|
protected array $deferred = [];
|
||||||
|
|
||||||
|
protected ?string $sourceFile = null;
|
||||||
|
|
||||||
|
protected string $sourceKind = 'attribute';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array|Closure $handler
|
* @param array|Closure $handler
|
||||||
* @param array $parameters
|
* @param array $parameters
|
||||||
@@ -49,7 +65,7 @@ class Handler implements RequestHandlerInterface
|
|||||||
*/
|
*/
|
||||||
public function setRequestMethod(string $method): void
|
public function setRequestMethod(string $method): void
|
||||||
{
|
{
|
||||||
if ($method == 'HEAD') {
|
if ($method == 'HEAD' || $method == 'OPTIONS') {
|
||||||
$this->format = Kiri::getDi()->get(NoBody::class);
|
$this->format = Kiri::getDi()->get(NoBody::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +116,67 @@ class Handler implements RequestHandlerInterface
|
|||||||
return $this->handler[1];
|
return $this->handler[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $middlewares
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setMiddlewares(array $middlewares): void
|
||||||
|
{
|
||||||
|
$this->middlewares = $middlewares;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getMiddlewares(): array
|
||||||
|
{
|
||||||
|
return $this->middlewares;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
* @param ServerRequestInterface $request
|
||||||
@@ -112,8 +189,26 @@ class Handler implements RequestHandlerInterface
|
|||||||
|
|
||||||
$data = call_user_func([$controller, $this->handler[1]], ...$this->parameters);
|
$data = call_user_func([$controller, $this->handler[1]], ...$this->parameters);
|
||||||
|
|
||||||
|
$this->executeDeferred();
|
||||||
|
|
||||||
/** 根据返回类型 */
|
/** 根据返回类型 */
|
||||||
return $this->format->call($data);
|
return $this->format->call($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步执行 Defer 回调 — 委托 DeferExecutor 处理协程安全与上下文注入
|
||||||
|
*/
|
||||||
|
private function executeDeferred(): void
|
||||||
|
{
|
||||||
|
if (empty($this->deferred)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defers = $this->deferred;
|
||||||
|
$this->deferred = [];
|
||||||
|
|
||||||
|
DeferExecutor::run($defers);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use Kiri\Router\Base\AbstractHandler;
|
|||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use ReflectionException;
|
|
||||||
|
|
||||||
class HttpRequestHandler extends AbstractHandler implements RequestHandlerInterface
|
class HttpRequestHandler extends AbstractHandler implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
|
|||||||
+23
-1
@@ -17,6 +17,7 @@ use Kiri\Router\Interface\OnRequestInterface;
|
|||||||
use Psr\Http\Message\RequestInterface;
|
use Psr\Http\Message\RequestInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
use Swoole\Http\Request;
|
use Swoole\Http\Request;
|
||||||
use Swoole\Http\Response;
|
use Swoole\Http\Response;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -33,6 +34,9 @@ class OnRequest implements OnRequestInterface
|
|||||||
public RouterCollector $router;
|
public RouterCollector $router;
|
||||||
|
|
||||||
|
|
||||||
|
public DataGrip $dataGrip;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ExceptionHandlerInterface
|
* @var ExceptionHandlerInterface
|
||||||
*/
|
*/
|
||||||
@@ -58,8 +62,9 @@ class OnRequest implements OnRequestInterface
|
|||||||
*/
|
*/
|
||||||
public function __construct(public ResponseInterface $response, DataGrip $dataGrip)
|
public function __construct(public ResponseInterface $response, DataGrip $dataGrip)
|
||||||
{
|
{
|
||||||
|
$this->dataGrip = $dataGrip;
|
||||||
$this->responseEmitter = $this->response->emmit;
|
$this->responseEmitter = $this->response->emmit;
|
||||||
$exception = \config('exception.http');
|
$exception = \config('servers.request.exception');
|
||||||
if (!in_array(ExceptionHandlerInterface::class, class_implements($exception))) {
|
if (!in_array(ExceptionHandlerInterface::class, class_implements($exception))) {
|
||||||
$exception = ExceptionHandlerDispatcher::class;
|
$exception = ExceptionHandlerDispatcher::class;
|
||||||
}
|
}
|
||||||
@@ -76,18 +81,35 @@ class OnRequest implements OnRequestInterface
|
|||||||
public function onRequest(Request $request, Response $response): void
|
public function onRequest(Request $request, Response $response): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$this->setResponseHeaders($response, $this->response->headers);
|
||||||
|
|
||||||
/** @var CQ $PsrRequest */
|
/** @var CQ $PsrRequest */
|
||||||
Context::set(ResponseInterface::class, new ConstrictResponse($this->response->contentType));
|
Context::set(ResponseInterface::class, new ConstrictResponse($this->response->contentType));
|
||||||
$PsrRequest = Context::set(RequestInterface::class, CQ::builder($request));
|
$PsrRequest = Context::set(RequestInterface::class, CQ::builder($request));
|
||||||
|
$this->router = $this->dataGrip->get(ROUTER_TYPE_HTTP);
|
||||||
|
|
||||||
CoordinatorManager::utility(Coordinator::WORKER_START)->yield();
|
CoordinatorManager::utility(Coordinator::WORKER_START)->yield();
|
||||||
|
|
||||||
$PsrResponse = $this->router->query($request->server['path_info'], $request->getMethod())->run($PsrRequest);
|
$PsrResponse = $this->router->query($request->server['path_info'], $request->getMethod())->run($PsrRequest);
|
||||||
} catch (Throwable $throwable) {
|
} catch (Throwable $throwable) {
|
||||||
|
\Kiri::getLogger()->json_log($throwable);
|
||||||
$PsrResponse = $this->exception->emit($throwable, $this->constrictResponse);
|
$PsrResponse = $this->exception->emit($throwable, $this->constrictResponse);
|
||||||
} finally {
|
} finally {
|
||||||
$this->responseEmitter->response($PsrResponse, $response, $PsrRequest);
|
$this->responseEmitter->response($PsrResponse, $response, $PsrRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Response $response
|
||||||
|
* @param array $headers
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function setResponseHeaders(Response $response, array $headers): void
|
||||||
|
{
|
||||||
|
foreach ($headers as $key => $header) {
|
||||||
|
$response->header($key, $header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+839
-829
File diff suppressed because it is too large
Load Diff
+419
-381
@@ -15,426 +15,464 @@ class Response implements ResponseInterface
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ContentType
|
* @var ContentType
|
||||||
*/
|
*/
|
||||||
public ContentType $contentType = ContentType::JSON;
|
public ContentType $contentType = ContentType::JSON;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string|ResponseEmitterInterface
|
* @var array
|
||||||
*/
|
*/
|
||||||
public string|ResponseEmitterInterface $emmit = SwooleHttpResponseEmitter::class;
|
public array $headers = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化
|
* @var string|ResponseEmitterInterface
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public string|ResponseEmitterInterface $emmit = SwooleHttpResponseEmitter::class;
|
||||||
{
|
|
||||||
$this->contentType = \config('response.content-type', ContentType::JSON);
|
|
||||||
$this->emmit = \config('response.emmit', SwooleHttpResponseEmitter::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return void
|
|
||||||
* @throws
|
|
||||||
*/
|
|
||||||
public function init(): void
|
|
||||||
{
|
|
||||||
if (is_string($this->emmit)) {
|
|
||||||
$this->emmit = di($this->emmit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ContentType $contentType
|
* 初始化
|
||||||
* @return Response
|
*/
|
||||||
*/
|
public function __construct()
|
||||||
public function withContentType(ContentType $contentType): ResponseInterface
|
{
|
||||||
{
|
$this->contentType = \config('servers.response.content-type', ContentType::JSON);
|
||||||
return $this->__call__(__FUNCTION__, $contentType);
|
$this->headers = \config('servers.response.headers', []);
|
||||||
}
|
$this->emmit = \config('servers.response.emmit', SwooleHttpResponseEmitter::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public function init(): void
|
||||||
|
{
|
||||||
|
if (is_string($this->emmit)) {
|
||||||
|
$this->emmit = di($this->emmit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $content
|
* @param ContentType $contentType
|
||||||
* @param int $statusCode
|
*
|
||||||
* @return ResponseInterface
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function json(array $content, int $statusCode = 200): ResponseInterface
|
public function withContentType(ContentType $contentType): ResponseInterface
|
||||||
{
|
{
|
||||||
return $this->__call__(__FUNCTION__, $content, $statusCode);
|
return $this->__call__(__FUNCTION__, $contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $content
|
* @param array $content
|
||||||
* @param int $statusCode
|
* @param int $statusCode
|
||||||
* @return ResponseInterface
|
*
|
||||||
*/
|
* @return ResponseInterface
|
||||||
public function xml(array $content, int $statusCode = 200): ResponseInterface
|
*/
|
||||||
{
|
public function json(array $content, int $statusCode = 200): ResponseInterface
|
||||||
return $this->__call__(__FUNCTION__, $content, $statusCode);
|
{
|
||||||
}
|
return $this->__call__(__FUNCTION__, $content, $statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $url
|
||||||
|
* @param array $params
|
||||||
|
* @param int $statusCode
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function redirectTo(string $url, array $params = [], int $statusCode = 302): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->__call__(__FUNCTION__, $url, $params, $statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $content
|
||||||
|
* @param int $statusCode
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function xml(array $content, int $statusCode = 200): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->__call__(__FUNCTION__, $content, $statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $content
|
* @param string $content
|
||||||
* @param int $statusCode
|
* @param int $statusCode
|
||||||
* @return ResponseInterface
|
*
|
||||||
*/
|
* @return ResponseInterface
|
||||||
public function html(string $content = '', int $statusCode = 200): ResponseInterface
|
*/
|
||||||
{
|
public function html(string $content = '', int $statusCode = 200): ResponseInterface
|
||||||
return $this->__call__(__FUNCTION__, $content, $statusCode);
|
{
|
||||||
}
|
return $this->__call__(__FUNCTION__, $content, $statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $content
|
* @param string $content
|
||||||
* @param int $statusCode
|
* @param int $statusCode
|
||||||
* @return ResponseInterface
|
*
|
||||||
*/
|
* @return ResponseInterface
|
||||||
public function sendfile(string $content, int $statusCode = 200): ResponseInterface
|
*/
|
||||||
{
|
public function sendfile(string $content, int $statusCode = 200): ResponseInterface
|
||||||
return $this->__call__(__FUNCTION__, $content, $statusCode);
|
{
|
||||||
}
|
return $this->__call__(__FUNCTION__, $content, $statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $data
|
* @param mixed $data
|
||||||
* @param int $statusCode
|
* @param int $statusCode
|
||||||
* @param ContentType $type
|
* @param ContentType $type
|
||||||
* @return Response
|
*
|
||||||
*/
|
* @return Response
|
||||||
public function write(mixed $data, int $statusCode = 200, ContentType $type = ContentType::HTML): ResponseInterface
|
*/
|
||||||
{
|
public function write(mixed $data, int $statusCode = 200, ContentType $type = ContentType::HTML): ResponseInterface
|
||||||
return $this->__call__(__FUNCTION__, $data, $statusCode, $type);
|
{
|
||||||
}
|
return $this->__call__(__FUNCTION__, $data, $statusCode, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $method
|
* @param string $method
|
||||||
* @param mixed ...$params
|
* @param mixed ...$params
|
||||||
* @return mixed
|
*
|
||||||
*/
|
* @return mixed
|
||||||
private function __call__(string $method, ...$params): mixed
|
*/
|
||||||
{
|
private function __call__(string $method, ...$params): mixed
|
||||||
return \response()->{$method}(...$params);
|
{
|
||||||
}
|
return \response()->{$method}(...$params);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $cookies
|
* @param array $cookies
|
||||||
* @return ResponseInterface
|
*
|
||||||
*/
|
* @return ResponseInterface
|
||||||
public function withCookieParams(array $cookies): ResponseInterface
|
*/
|
||||||
{
|
public function withCookieParams(array $cookies): ResponseInterface
|
||||||
return $this->__call__(__FUNCTION__, $cookies);
|
{
|
||||||
}
|
return $this->__call__(__FUNCTION__, $cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getCookieParams(): array
|
public function getCookieParams(): array
|
||||||
{
|
{
|
||||||
return $this->__call__(__FUNCTION__);
|
return $this->__call__(__FUNCTION__);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the HTTP protocol version as a string.
|
* Retrieves the HTTP protocol version as a string.
|
||||||
*
|
*
|
||||||
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
|
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
|
||||||
*
|
*
|
||||||
* @return string HTTP protocol version.
|
* @return string HTTP protocol version.
|
||||||
*/
|
*/
|
||||||
public function getProtocolVersion(): string
|
public function getProtocolVersion(): string
|
||||||
{
|
{
|
||||||
// TODO: Implement getProtocolVersion() method.
|
// TODO: Implement getProtocolVersion() method.
|
||||||
return $this->__call__(__FUNCTION__);
|
return $this->__call__(__FUNCTION__);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance with the specified HTTP protocol version.
|
* Return an instance with the specified HTTP protocol version.
|
||||||
*
|
*
|
||||||
* The version string MUST contain only the HTTP version number (e.g.,
|
* The version string MUST contain only the HTTP version number (e.g.,
|
||||||
* "1.1", "1.0").
|
* "1.1", "1.0").
|
||||||
*
|
*
|
||||||
* This method MUST be implemented in such a way as to retain the
|
* This method MUST be implemented in such a way as to retain the
|
||||||
* immutability of the message, and MUST return an instance that has the
|
* immutability of the message, and MUST return an instance that has the
|
||||||
* new protocol version.
|
* new protocol version.
|
||||||
*
|
*
|
||||||
* @param string $version HTTP protocol version
|
* @param string $version HTTP protocol version
|
||||||
* @return static
|
*
|
||||||
*/
|
* @return static
|
||||||
public function withProtocolVersion(string $version): ResponseInterface
|
*/
|
||||||
{
|
public function withProtocolVersion(string $version): ResponseInterface
|
||||||
// TODO: Implement withProtocolVersion() method.
|
{
|
||||||
return $this->__call__(__FUNCTION__, $version);
|
// TODO: Implement withProtocolVersion() method.
|
||||||
}
|
return $this->__call__(__FUNCTION__, $version);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all message header values.
|
* Retrieves all message header values.
|
||||||
*
|
*
|
||||||
* The keys represent the header name as it will be sent over the wire, and
|
* The keys represent the header name as it will be sent over the wire, and
|
||||||
* each value is an array of strings associated with the header.
|
* each value is an array of strings associated with the header.
|
||||||
*
|
*
|
||||||
* // Represent the headers as a string
|
* // Represent the headers as a string
|
||||||
* foreach ($message->getHeaders() as $name => $values) {
|
* foreach ($message->getHeaders() as $name => $values) {
|
||||||
* echo $name . ": " . implode(", ", $values);
|
* echo $name . ": " . implode(", ", $values);
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* // Emit headers iteratively:
|
* // Emit headers iteratively:
|
||||||
* foreach ($message->getHeaders() as $name => $values) {
|
* foreach ($message->getHeaders() as $name => $values) {
|
||||||
* foreach ($values as $value) {
|
* foreach ($values as $value) {
|
||||||
* header(sprintf('%s: %s', $name, $value), false);
|
* header(sprintf('%s: %s', $name, $value), false);
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||||
* exact case in which headers were originally specified.
|
* exact case in which headers were originally specified.
|
||||||
*
|
*
|
||||||
* @return string[][] Returns an associative array of the message's headers. Each
|
* @return string[][] Returns an associative array of the message's headers. Each
|
||||||
* key MUST be a header name, and each value MUST be an array of strings
|
* key MUST be a header name, and each value MUST be an array of strings
|
||||||
* for that header.
|
* for that header.
|
||||||
*/
|
*/
|
||||||
public function getHeaders(): array
|
public function getHeaders(): array
|
||||||
{
|
{
|
||||||
// TODO: Implement getHeaders() method.
|
// TODO: Implement getHeaders() method.
|
||||||
return $this->__call__(__FUNCTION__);
|
return $this->__call__(__FUNCTION__);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a header exists by the given case-insensitive name.
|
* Checks if a header exists by the given case-insensitive name.
|
||||||
*
|
*
|
||||||
* @param string $name Case-insensitive header field name.
|
* @param string $name Case-insensitive header field name.
|
||||||
* @return bool Returns true if any header names match the given header
|
*
|
||||||
* name using a case-insensitive string comparison. Returns false if
|
* @return bool Returns true if any header names match the given header
|
||||||
* no matching header name is found in the message.
|
* name using a case-insensitive string comparison. Returns false if
|
||||||
*/
|
* no matching header name is found in the message.
|
||||||
public function hasHeader(string $name): bool
|
*/
|
||||||
{
|
public function hasHeader(string $name): bool
|
||||||
// TODO: Implement hasHeader() method.
|
{
|
||||||
return $this->__call__(__FUNCTION__, $name);
|
// TODO: Implement hasHeader() method.
|
||||||
}
|
return $this->__call__(__FUNCTION__, $name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a message header value by the given case-insensitive name.
|
* Retrieves a message header value by the given case-insensitive name.
|
||||||
*
|
*
|
||||||
* This method returns an array of all the header values of the given
|
* This method returns an array of all the header values of the given
|
||||||
* case-insensitive header name.
|
* case-insensitive header name.
|
||||||
*
|
*
|
||||||
* If the header does not appear in the message, this method MUST return an
|
* If the header does not appear in the message, this method MUST return an
|
||||||
* empty array.
|
* empty array.
|
||||||
*
|
*
|
||||||
* @param string $name Case-insensitive header field name.
|
* @param string $name Case-insensitive header field name.
|
||||||
* @return string[] An array of string values as provided for the given
|
*
|
||||||
* header. If the header does not appear in the message, this method MUST
|
* @return string[] An array of string values as provided for the given
|
||||||
* return an empty array.
|
* header. If the header does not appear in the message, this method MUST
|
||||||
*/
|
* return an empty array.
|
||||||
public function getHeader(string $name): array
|
*/
|
||||||
{
|
public function getHeader(string $name): array
|
||||||
// TODO: Implement getHeader() method.
|
{
|
||||||
return $this->__call__(__FUNCTION__, $name);
|
// TODO: Implement getHeader() method.
|
||||||
}
|
return $this->__call__(__FUNCTION__, $name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a comma-separated string of the values for a single header.
|
* Retrieves a comma-separated string of the values for a single header.
|
||||||
*
|
*
|
||||||
* This method returns all the header values of the given
|
* This method returns all the header values of the given
|
||||||
* case-insensitive header name as a string concatenated together using
|
* case-insensitive header name as a string concatenated together using
|
||||||
* a comma.
|
* a comma.
|
||||||
*
|
*
|
||||||
* NOTE: Not all header values may be appropriately represented using
|
* NOTE: Not all header values may be appropriately represented using
|
||||||
* comma concatenation. For such headers, use getHeader() instead
|
* comma concatenation. For such headers, use getHeader() instead
|
||||||
* and supply your own delimiter when concatenating.
|
* and supply your own delimiter when concatenating.
|
||||||
*
|
*
|
||||||
* If the header does not appear in the message, this method MUST return
|
* If the header does not appear in the message, this method MUST return
|
||||||
* an empty string.
|
* an empty string.
|
||||||
*
|
*
|
||||||
* @param string $name Case-insensitive header field name.
|
* @param string $name Case-insensitive header field name.
|
||||||
* @return string A string of values as provided for the given header
|
*
|
||||||
* concatenated together using a comma. If the header does not appear in
|
* @return string A string of values as provided for the given header
|
||||||
* the message, this method MUST return an empty string.
|
* concatenated together using a comma. If the header does not appear in
|
||||||
*/
|
* the message, this method MUST return an empty string.
|
||||||
public function getHeaderLine(string $name): string
|
*/
|
||||||
{
|
public function getHeaderLine(string $name): string
|
||||||
// TODO: Implement getHeaderLine() method.
|
{
|
||||||
return $this->__call__(__FUNCTION__, $name);
|
// TODO: Implement getHeaderLine() method.
|
||||||
}
|
return $this->__call__(__FUNCTION__, $name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance with the provided value replacing the specified header.
|
* Return an instance with the provided value replacing the specified header.
|
||||||
*
|
*
|
||||||
* While header names are case-insensitive, the casing of the header will
|
* While header names are case-insensitive, the casing of the header will
|
||||||
* be preserved by this function, and returned from getHeaders().
|
* be preserved by this function, and returned from getHeaders().
|
||||||
*
|
*
|
||||||
* This method MUST be implemented in such a way as to retain the
|
* This method MUST be implemented in such a way as to retain the
|
||||||
* immutability of the message, and MUST return an instance that has the
|
* immutability of the message, and MUST return an instance that has the
|
||||||
* new and/or updated header and value.
|
* new and/or updated header and value.
|
||||||
*
|
*
|
||||||
* @param string $name Case-insensitive header field name.
|
* @param string $name Case-insensitive header field name.
|
||||||
* @param string|string[] $value Header value(s).
|
* @param string|string[] $value Header value(s).
|
||||||
* @return static
|
*
|
||||||
* @throws
|
* @return static
|
||||||
*/
|
* @throws
|
||||||
public function withHeader(string $name, $value): ResponseInterface
|
*/
|
||||||
{
|
public function withHeader(string $name, $value): ResponseInterface
|
||||||
// TODO: Implement withHeader() method.
|
{
|
||||||
return $this->__call__(__FUNCTION__, $name, $value);
|
// TODO: Implement withHeader() method.
|
||||||
}
|
return $this->__call__(__FUNCTION__, $name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $headers
|
* @param array $headers
|
||||||
* @return ResponseInterface
|
*
|
||||||
*/
|
* @return ResponseInterface
|
||||||
public function withHeaders(array $headers): ResponseInterface
|
*/
|
||||||
{
|
public function withHeaders(array $headers): ResponseInterface
|
||||||
return $this->__call__(__FUNCTION__, $headers);
|
{
|
||||||
}
|
return $this->__call__(__FUNCTION__, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance with the specified header appended with the given value.
|
* Return an instance with the specified header appended with the given value.
|
||||||
*
|
*
|
||||||
* Existing values for the specified header will be maintained. The new
|
* Existing values for the specified header will be maintained. The new
|
||||||
* value(s) will be appended to the existing list. If the header did not
|
* value(s) will be appended to the existing list. If the header did not
|
||||||
* exist previously, it will be added.
|
* exist previously, it will be added.
|
||||||
*
|
*
|
||||||
* This method MUST be implemented in such a way as to retain the
|
* This method MUST be implemented in such a way as to retain the
|
||||||
* immutability of the message, and MUST return an instance that has the
|
* immutability of the message, and MUST return an instance that has the
|
||||||
* new header and/or value.
|
* new header and/or value.
|
||||||
*
|
*
|
||||||
* @param string $name Case-insensitive header field name to add.
|
* @param string $name Case-insensitive header field name to add.
|
||||||
* @param string|string[] $value Header value(s).
|
* @param string|string[] $value Header value(s).
|
||||||
* @return static
|
*
|
||||||
* @throws
|
* @return static
|
||||||
*/
|
* @throws
|
||||||
public function withAddedHeader(string $name, $value): ResponseInterface
|
*/
|
||||||
{
|
public function withAddedHeader(string $name, $value): ResponseInterface
|
||||||
// TODO: Implement withAddedHeader() method.
|
{
|
||||||
return $this->__call__(__FUNCTION__, $name, $value);
|
// TODO: Implement withAddedHeader() method.
|
||||||
}
|
return $this->__call__(__FUNCTION__, $name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance without the specified header.
|
* Return an instance without the specified header.
|
||||||
*
|
*
|
||||||
* Header resolution MUST be done without case-sensitivity.
|
* Header resolution MUST be done without case-sensitivity.
|
||||||
*
|
*
|
||||||
* This method MUST be implemented in such a way as to retain the
|
* This method MUST be implemented in such a way as to retain the
|
||||||
* immutability of the message, and MUST return an instance that removes
|
* immutability of the message, and MUST return an instance that removes
|
||||||
* the named header.
|
* the named header.
|
||||||
*
|
*
|
||||||
* @param string $name Case-insensitive header field name to remove.
|
* @param string $name Case-insensitive header field name to remove.
|
||||||
* @return static
|
*
|
||||||
*/
|
* @return static
|
||||||
public function withoutHeader(string $name): ResponseInterface
|
*/
|
||||||
{
|
public function withoutHeader(string $name): ResponseInterface
|
||||||
// TODO: Implement withoutHeader() method.
|
{
|
||||||
return $this->__call__(__FUNCTION__, $name);
|
// TODO: Implement withoutHeader() method.
|
||||||
}
|
return $this->__call__(__FUNCTION__, $name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the body of the message.
|
* Gets the body of the message.
|
||||||
*
|
*
|
||||||
* @return StreamInterface Returns the body as a stream.
|
* @return StreamInterface Returns the body as a stream.
|
||||||
*/
|
*/
|
||||||
public function getBody(): StreamInterface
|
public function getBody(): StreamInterface
|
||||||
{
|
{
|
||||||
// TODO: Implement getBody() method.
|
// TODO: Implement getBody() method.
|
||||||
return $this->__call__(__FUNCTION__);
|
return $this->__call__(__FUNCTION__);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance with the specified message body.
|
* Return an instance with the specified message body.
|
||||||
*
|
*
|
||||||
* The body MUST be a StreamInterface object.
|
* The body MUST be a StreamInterface object.
|
||||||
*
|
*
|
||||||
* This method MUST be implemented in such a way as to retain the
|
* This method MUST be implemented in such a way as to retain the
|
||||||
* immutability of the message, and MUST return a new instance that has the
|
* immutability of the message, and MUST return a new instance that has the
|
||||||
* new body stream.
|
* new body stream.
|
||||||
*
|
*
|
||||||
* @param StreamInterface $body Body.
|
* @param StreamInterface $body Body.
|
||||||
* @return static
|
*
|
||||||
* @throws
|
* @return static
|
||||||
*/
|
* @throws
|
||||||
public function withBody(StreamInterface $body): ResponseInterface
|
*/
|
||||||
{
|
public function withBody(StreamInterface $body): ResponseInterface
|
||||||
// TODO: Implement withBody() method.
|
{
|
||||||
return $this->__call__(__FUNCTION__, $body);
|
// TODO: Implement withBody() method.
|
||||||
}
|
return $this->__call__(__FUNCTION__, $body);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the response status code.
|
* Gets the response status code.
|
||||||
*
|
*
|
||||||
* The status code is a 3-digit integer result code of the server's attempt
|
* The status code is a 3-digit integer result code of the server's attempt
|
||||||
* to understand and satisfy the request.
|
* to understand and satisfy the request.
|
||||||
*
|
*
|
||||||
* @return int Status code.
|
* @return int Status code.
|
||||||
*/
|
*/
|
||||||
public function getStatusCode(): int
|
public function getStatusCode(): int
|
||||||
{
|
{
|
||||||
// TODO: Implement getStatusCode() method.
|
// TODO: Implement getStatusCode() method.
|
||||||
return $this->__call__(__FUNCTION__);
|
return $this->__call__(__FUNCTION__);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $content
|
* @param string $content
|
||||||
* @param int $statusCode
|
* @param int $statusCode
|
||||||
* @param ContentType $contentType
|
* @param ContentType $contentType
|
||||||
* @return ResponseInterface
|
*
|
||||||
*/
|
* @return ResponseInterface
|
||||||
public function raw(string $content, int $statusCode = 200, ContentType $contentType = ContentType::JSON): ResponseInterface
|
*/
|
||||||
{
|
public function raw(string $content, int $statusCode = 200, ContentType $contentType = ContentType::JSON): ResponseInterface
|
||||||
return $this->__call__(__FUNCTION__, $content, $statusCode, $contentType);
|
{
|
||||||
}
|
return $this->__call__(__FUNCTION__, $content, $statusCode, $contentType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance with the specified status code and, optionally, reason phrase.
|
* Return an instance with the specified status code and, optionally, reason phrase.
|
||||||
*
|
*
|
||||||
* If no reason phrase is specified, implementations MAY choose to default
|
* If no reason phrase is specified, implementations MAY choose to default
|
||||||
* to the RFC 7231 or IANA recommended reason phrase for the response's
|
* to the RFC 7231 or IANA recommended reason phrase for the response's
|
||||||
* status code.
|
* status code.
|
||||||
*
|
*
|
||||||
* This method MUST be implemented in such a way as to retain the
|
* This method MUST be implemented in such a way as to retain the
|
||||||
* immutability of the message, and MUST return an instance that has the
|
* immutability of the message, and MUST return an instance that has the
|
||||||
* updated status and reason phrase.
|
* updated status and reason phrase.
|
||||||
*
|
*
|
||||||
* @link http://tools.ietf.org/html/rfc7231#section-6
|
* @link http://tools.ietf.org/html/rfc7231#section-6
|
||||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||||
* @param int $code The 3-digit integer result code to set.
|
*
|
||||||
* @param string $reasonPhrase The reason phrase to use with the
|
* @param int $code The 3-digit integer result code to set.
|
||||||
* provided status code; if none is provided, implementations MAY
|
* @param string $reasonPhrase The reason phrase to use with the
|
||||||
* use the defaults as suggested in the HTTP specification.
|
* provided status code; if none is provided, implementations MAY
|
||||||
* @return static
|
* use the defaults as suggested in the HTTP specification.
|
||||||
* @throws
|
*
|
||||||
*/
|
* @return static
|
||||||
public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
|
* @throws
|
||||||
{
|
*/
|
||||||
// TODO: Implement withStatus() method.
|
public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
|
||||||
return $this->__call__(__FUNCTION__, $code, $reasonPhrase);
|
{
|
||||||
}
|
// TODO: Implement withStatus() method.
|
||||||
|
return $this->__call__(__FUNCTION__, $code, $reasonPhrase);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the response reason phrase associated with the status code.
|
* Gets the response reason phrase associated with the status code.
|
||||||
*
|
*
|
||||||
* Because a reason phrase is not a required element in a response
|
* Because a reason phrase is not a required element in a response
|
||||||
* status line, the reason phrase value MAY be null. Implementations MAY
|
* status line, the reason phrase value MAY be null. Implementations MAY
|
||||||
* choose to return the default RFC 7231 recommended reason phrase (or those
|
* choose to return the default RFC 7231 recommended reason phrase (or those
|
||||||
* listed in the IANA HTTP Status Code Registry) for the response's
|
* listed in the IANA HTTP Status Code Registry) for the response's
|
||||||
* status code.
|
* status code.
|
||||||
*
|
*
|
||||||
* @link http://tools.ietf.org/html/rfc7231#section-6
|
* @link http://tools.ietf.org/html/rfc7231#section-6
|
||||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||||
* @return string Reason phrase; must return an empty string if none present.
|
* @return string Reason phrase; must return an empty string if none present.
|
||||||
*/
|
*/
|
||||||
public function getReasonPhrase(): string
|
public function getReasonPhrase(): string
|
||||||
{
|
{
|
||||||
// TODO: Implement getReasonPhrase() method.
|
// TODO: Implement getReasonPhrase() method.
|
||||||
return $this->__call__(__FUNCTION__);
|
return $this->__call__(__FUNCTION__);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = [],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+414
-178
@@ -4,14 +4,17 @@ declare(strict_types=1);
|
|||||||
namespace Kiri\Router;
|
namespace Kiri\Router;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Kiri\Di\HotReloadState;
|
||||||
use Kiri\Server\Events\OnWorkerStart;
|
use Kiri\Server\Events\OnWorkerStart;
|
||||||
use Kiri;
|
use Kiri;
|
||||||
use Kiri\Abstracts\CoordinatorManager;
|
use Kiri\Abstracts\CoordinatorManager;
|
||||||
use Kiri\Coordinator;
|
use Kiri\Coordinator;
|
||||||
|
use Kiri\Router\RouteArtifactState;
|
||||||
use Kiri\Router\Validator\ValidatorMiddleware;
|
use Kiri\Router\Validator\ValidatorMiddleware;
|
||||||
use Kiri\Router\Base\Middleware as MiddlewareManager;
|
use Kiri\Router\Base\Middleware as MiddlewareManager;
|
||||||
use Kiri\Router\Constrict\RequestMethod;
|
use Kiri\Router\Constrict\RequestMethod;
|
||||||
use Psr\Container\ContainerInterface;
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -26,219 +29,452 @@ class Router
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
const array METHODS = [RequestMethod::REQUEST_POST, RequestMethod::REQUEST_GET, RequestMethod::REQUEST_OPTIONS, RequestMethod::REQUEST_DELETE, RequestMethod::REQUEST_PUT, RequestMethod::REQUEST_HEAD];
|
const array METHODS = [RequestMethod::REQUEST_POST, RequestMethod::REQUEST_GET, RequestMethod::REQUEST_OPTIONS, RequestMethod::REQUEST_DELETE, RequestMethod::REQUEST_PUT, RequestMethod::REQUEST_HEAD];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
private static string $type = ROUTER_TYPE_HTTP;
|
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
|
* @param string $name
|
||||||
* @param Closure $closure
|
* @param Closure $closure
|
||||||
*/
|
*/
|
||||||
public static function addServer(string $name, Closure $closure): void
|
public static function addServer(string $name, Closure $closure): void
|
||||||
{
|
{
|
||||||
static::$type = $name;
|
static::$type = $name;
|
||||||
$closure();
|
$closure();
|
||||||
static::$type = ROUTER_TYPE_HTTP;
|
static::$type = ROUTER_TYPE_HTTP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Closure $handler
|
* @param Closure $handler
|
||||||
*/
|
*/
|
||||||
public static function jsonp(Closure $handler): void
|
public static function jsonp(Closure $handler): void
|
||||||
{
|
{
|
||||||
static::$type = 'json-rpc';
|
static::$type = 'json-rpc';
|
||||||
$handler();
|
$handler();
|
||||||
static::$type = ROUTER_TYPE_HTTP;
|
static::$type = ROUTER_TYPE_HTTP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $route
|
* @param string $route
|
||||||
* @param string $handler
|
* @param string $handler
|
||||||
* @throws
|
*
|
||||||
*/
|
* @throws
|
||||||
public static function post(string $route, string $handler): void
|
*/
|
||||||
{
|
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);
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
}
|
$router->addRoute([RequestMethod::REQUEST_POST], $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $route
|
* @param string $route
|
||||||
* @param string $handler
|
* @param string $handler
|
||||||
* @throws
|
*
|
||||||
*/
|
* @throws
|
||||||
public static function get(string $route, string $handler): void
|
*/
|
||||||
{
|
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);
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
}
|
$router->addRoute([RequestMethod::REQUEST_GET], $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $route
|
* @param string $route
|
||||||
* @param string $handler
|
* @param string $handler
|
||||||
* @throws
|
*
|
||||||
*/
|
* @throws
|
||||||
public static function options(string $route, string $handler): void
|
*/
|
||||||
{
|
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);
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
}
|
$router->addRoute([RequestMethod::REQUEST_OPTIONS], $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $route
|
* @param string $route
|
||||||
* @param string $handler
|
* @param string $handler
|
||||||
* @throws
|
*
|
||||||
*/
|
* @throws
|
||||||
public static function any(string $route, string $handler): void
|
*/
|
||||||
{
|
public static function any(string $route, string $handler): void
|
||||||
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
{
|
||||||
$router->addRoute(self::METHODS, $route, $handler);
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
}
|
$router->addRoute(self::METHODS, $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $route
|
* @param string $route
|
||||||
* @param string $handler
|
* @param string $handler
|
||||||
* @throws
|
*
|
||||||
*/
|
* @throws
|
||||||
public static function delete(string $route, string $handler): void
|
*/
|
||||||
{
|
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);
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
}
|
$router->addRoute([RequestMethod::REQUEST_DELETE], $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $route
|
* @param string $route
|
||||||
* @param string $handler
|
* @param string $handler
|
||||||
* @throws
|
*
|
||||||
*/
|
* @throws
|
||||||
public static function head(string $route, string $handler): void
|
*/
|
||||||
{
|
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);
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
}
|
$router->addRoute([RequestMethod::REQUEST_HEAD], $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $route
|
* @param string $route
|
||||||
* @param string $handler
|
* @param string $handler
|
||||||
* @throws
|
*
|
||||||
*/
|
* @throws
|
||||||
public static function put(string $route, string $handler): void
|
*/
|
||||||
{
|
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);
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
}
|
$router->addRoute([RequestMethod::REQUEST_PUT], $route, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array|RequestMethod $methods
|
* @param array|RequestMethod $methods
|
||||||
* @param string $route
|
* @param string $route
|
||||||
* @param string|array $handler
|
* @param string|array $handler
|
||||||
*/
|
*/
|
||||||
public static function addRoute(array|RequestMethod $methods, string $route, string|array $handler): void
|
public static function addRoute(array|RequestMethod $methods, string $route, string|array $handler): void
|
||||||
{
|
{
|
||||||
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
if ($methods instanceof RequestMethod) {
|
if ($methods instanceof RequestMethod) {
|
||||||
$methods = [$methods];
|
$methods = [$methods];
|
||||||
}
|
}
|
||||||
$router->addRoute($methods, $route, $handler);
|
$router->addRoute($methods, $route, $handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $config
|
* @param array $config
|
||||||
* @param Closure $closure
|
* @param Closure $closure
|
||||||
* @throws
|
*
|
||||||
*/
|
* @throws
|
||||||
public static function group(array $config, Closure $closure): void
|
*/
|
||||||
{
|
public static function group(array $config, Closure $closure): void
|
||||||
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
{
|
||||||
|
$router = Kiri::getDi()->get(DataGrip::class)->get(static::$type);
|
||||||
|
|
||||||
$router->groupTack[] = $config;
|
$router->groupTack[] = $config;
|
||||||
|
|
||||||
call_user_func($closure);
|
call_user_func($closure);
|
||||||
|
|
||||||
array_pop($router->groupTack);
|
array_pop($router->groupTack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws
|
* 扫描并构建路由表
|
||||||
*/
|
*
|
||||||
public function scan_build_route(): void
|
* Master 进程:执行完整扫描(路由文件加载 + app 目录扫描 + DeferRegistry 注入)
|
||||||
{
|
* Worker 进程(首次启动):仅加载路由文件注册路由表,跳过全量 app 扫描
|
||||||
$coordinator = CoordinatorManager::utility(Coordinator::WORKER_START);
|
* 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();
|
||||||
|
|
||||||
$this->read_dir_file(APP_PATH . 'routes');
|
$changedFiles = $container->get(HotReloadState::class)->consume();
|
||||||
|
|
||||||
$container = Kiri::getDi();
|
// Worker 首次启动(无变更文件 + Master 已完成扫描):
|
||||||
$scanner = $container->get(Kiri\Di\Scanner::class);
|
// 重新 include 路由文件(Router::get/post 显式注册) + 基于 Master 扫描清单重建注解路由
|
||||||
$scanner->load_directory(APP_PATH . 'app/Controller');
|
// 避免 opcache_compile_file,仅用 Reflection 重建路由,内存开销极小
|
||||||
$this->reset($container);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
$coordinator->done();
|
// 标记首次扫描完成(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');
|
||||||
|
|
||||||
|
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
|
* @param ContainerInterface $container
|
||||||
* @return void
|
*
|
||||||
* @throws
|
* @return void
|
||||||
*/
|
* @throws
|
||||||
public function reset(ContainerInterface $container): void
|
*/
|
||||||
{
|
public function reset(ContainerInterface $container): void
|
||||||
$router = $container->get(DataGrip::class)->get(static::$type);
|
{
|
||||||
foreach ($router->getMethods() as $name => $method) {
|
$router = $container->get(DataGrip::class)->get(static::$type);
|
||||||
$middlewares = MiddlewareManager::get($method->getClass(), $method->getMethod());
|
if ((bool)config('servers.reload.scan.prebuild_http_handlers', false)) {
|
||||||
$validator = MiddlewareManager::getValidator($method->getClass(), $method->getMethod());
|
$router->warmHttpHandlers();
|
||||||
if (!is_null($validator)) {
|
}
|
||||||
array_unshift($middlewares, new ValidatorMiddleware($method->getClass(), $method->getMethod()));
|
}
|
||||||
}
|
|
||||||
$router->setHttpHandler($name, new HttpRequestHandler($middlewares, $method));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $path
|
* 基于 Master 扫描清单重建注解路由(轻量级,无文件 I/O)
|
||||||
* @return void
|
* 遍历 Scanner manifest 中的所有类,用 Reflection 重新发现 #[Route]/#[Get] 等注解
|
||||||
* @throws
|
* 避免 Worker 重复执行 opcache_compile_file,但确保注解路由不丢失
|
||||||
*/
|
*
|
||||||
private function read_dir_file($path): void
|
* @param ContainerInterface $container
|
||||||
{
|
* @return void
|
||||||
$files = glob($path . '/*');
|
*/
|
||||||
for ($i = 0; $i < count($files); $i++) {
|
private function rebuildAnnotationRoutes(ContainerInterface $container): void
|
||||||
$file = $files[$i];
|
{
|
||||||
if (is_dir($file)) {
|
$scanner = $container->get(Kiri\Di\Scanner::class);
|
||||||
$this->read_dir_file($file);
|
$scanConfig = array_merge(
|
||||||
} else {
|
config('servers.reload.scan', []),
|
||||||
$this->resolve_file($file);
|
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 $files
|
* @param $path
|
||||||
* @throws
|
*
|
||||||
*/
|
* @return void
|
||||||
private function resolve_file($files): void
|
* @throws
|
||||||
{
|
*/
|
||||||
try {
|
private function read_dir_file($path): void
|
||||||
include "$files";
|
{
|
||||||
} catch (\Throwable $throwable) {
|
$files = glob($path . '/*');
|
||||||
error($throwable);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+497
-251
@@ -6,12 +6,17 @@ namespace Kiri\Router;
|
|||||||
|
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Kiri\Router\Annotate\Defer;
|
||||||
|
use Kiri\Router\Defer\DeferRegistry;
|
||||||
use Kiri\Router\Base\NotFoundController;
|
use Kiri\Router\Base\NotFoundController;
|
||||||
use Kiri\Router\Constrict\RequestMethod;
|
use Kiri\Router\Constrict\RequestMethod;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use ReflectionException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use Traversable;
|
use Traversable;
|
||||||
use Kiri\Router\Base\Middleware;
|
use Kiri\Router\Base\Middleware;
|
||||||
use Kiri\Router\Format\ResponseFormat;
|
use Kiri\Router\Format\ResponseFormat;
|
||||||
|
use Kiri\Router\Validator\ValidatorMiddleware;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,300 +26,541 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private array $_item = [];
|
private array $_item = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private array $dump = [];
|
private array $dump = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
public array $groupTack = [];
|
public array $groupTack = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array<string, Handler>
|
* @var array<string, Handler|RouteEntry>
|
||||||
*/
|
*/
|
||||||
private array $methods = [];
|
private array $methods = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array<string, HttpRequestHandler>
|
* @var array<string, HttpRequestHandler>
|
||||||
*/
|
*/
|
||||||
protected array $httpHandler = [];
|
protected array $httpHandler = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Handler
|
* @var Handler
|
||||||
*/
|
*/
|
||||||
protected Handler $found;
|
protected Handler $found;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws
|
* @throws
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->found = new Handler([NotFoundController::class, 'fail'], [], ResponseFormat::class);
|
$this->found = new Handler([NotFoundController::class, 'fail'], [], ResponseFormat::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Handler[]
|
* @return array<string, Handler|RouteEntry>
|
||||||
*/
|
*/
|
||||||
public function getMethods(): array
|
public function getMethods(): array
|
||||||
{
|
{
|
||||||
return $this->methods;
|
return $this->methods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
public function clear(): void
|
||||||
* @param string $method
|
{
|
||||||
* @param HttpRequestHandler $handler
|
$this->_item = [];
|
||||||
* @return void
|
$this->dump = [];
|
||||||
*/
|
$this->groupTack = [];
|
||||||
public function setHttpHandler(string $method, HttpRequestHandler $handler): void
|
$this->methods = [];
|
||||||
{
|
$this->httpHandler = [];
|
||||||
$this->httpHandler[$method] = $handler;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @param string $method
|
||||||
*/
|
* @param HttpRequestHandler $handler
|
||||||
public function getDump(): array
|
* @return void
|
||||||
{
|
*/
|
||||||
return $this->dump;
|
public function setHttpHandler(string $method, HttpRequestHandler $handler): void
|
||||||
}
|
{
|
||||||
|
$this->httpHandler[$method] = $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Traversable
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getIterator(): Traversable
|
public function getDump(): array
|
||||||
{
|
{
|
||||||
return new \ArrayIterator($this->_item);
|
return $this->dump;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $method
|
* @return Traversable
|
||||||
* @param string $route
|
*/
|
||||||
* @param string|array|Closure $closure
|
public function getIterator(): Traversable
|
||||||
*/
|
{
|
||||||
public function addRoute(array $method, string $route, string|array|Closure $closure): void
|
return new \ArrayIterator($this->_item);
|
||||||
{
|
}
|
||||||
try {
|
|
||||||
$route = $this->_splicing_routing($route);
|
|
||||||
if ($closure instanceof Closure) {
|
|
||||||
$handler = di(ControllerInterpreter::class)->addRouteByClosure($closure);
|
|
||||||
} else {
|
|
||||||
$handler = $this->resolve($closure, di(ControllerInterpreter::class));
|
|
||||||
}
|
|
||||||
foreach ($method as $value) {
|
|
||||||
if ($value instanceof RequestMethod) {
|
|
||||||
$value = $value->getString();
|
|
||||||
}
|
|
||||||
$handler->setRequestMethod($value);
|
|
||||||
if (is_array($closure)) {
|
|
||||||
$closure[0] = is_object($closure[0]) ? get_class($closure[0]) : $closure;
|
|
||||||
} else if (is_string($closure)) {
|
|
||||||
$closure = explode('@', $closure);
|
|
||||||
}
|
|
||||||
$this->register($route, $value, $handler);
|
|
||||||
}
|
|
||||||
} catch (Throwable $throwable) {
|
|
||||||
error($throwable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @param array $method
|
||||||
*/
|
* @param string $route
|
||||||
public function dump(): array
|
* @param string|array|Closure $closure
|
||||||
{
|
*/
|
||||||
$array = [];
|
public function addRoute(array $method, string $route, string|array|Closure $closure): void
|
||||||
foreach ($this->methods as $methodPath => $handler) {
|
{
|
||||||
[$path, $method] = explode('_', $methodPath);
|
try {
|
||||||
|
$route = $this->_splicing_routing($route);
|
||||||
$controller = $handler instanceof Closure ? '\Closure' : $handler->getClass() . '::' . $handler->getMethod();
|
if ($closure instanceof Closure) {
|
||||||
$array[] = [
|
$handler = di(ControllerInterpreter::class)->addRouteByClosure($closure);
|
||||||
'path' => $path,
|
} else {
|
||||||
'method' => $method,
|
$handler = $this->resolve($closure, di(ControllerInterpreter::class));
|
||||||
'handler' => $controller
|
}
|
||||||
];
|
foreach ($method as $value) {
|
||||||
}
|
if ($value instanceof RequestMethod) {
|
||||||
return $array;
|
$value = $value->getString();
|
||||||
}
|
}
|
||||||
|
$handler->setRequestMethod($value);
|
||||||
|
if (is_array($closure)) {
|
||||||
|
$closure[0] = is_object($closure[0]) ? get_class($closure[0]) : $closure;
|
||||||
|
} else if (is_string($closure)) {
|
||||||
|
$closure = explode('@', $closure);
|
||||||
|
}
|
||||||
|
$this->register($route, $value, $handler);
|
||||||
|
}
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
\Kiri::getLogger()->json_log($throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string|array $closure
|
* @return array
|
||||||
* @param ControllerInterpreter $interpreter
|
*/
|
||||||
* @return Handler
|
public function dump(): array
|
||||||
* @throws
|
{
|
||||||
*/
|
$array = [];
|
||||||
private function resolve(string|array $closure, ControllerInterpreter $interpreter): Handler
|
foreach ($this->methods as $methodPath => $handler) {
|
||||||
{
|
[$path, $method] = explode('_', $methodPath);
|
||||||
$container = \Kiri::getDi();
|
|
||||||
if (is_array($closure)) {
|
$controller = $handler instanceof Closure ? '\Closure' : $handler->getClass() . '::' . $handler->getMethod();
|
||||||
if (is_string($closure[0])) {
|
$array[] = [
|
||||||
$closure[0] = $container->get($closure[0]);
|
'path' => $path,
|
||||||
}
|
'method' => $method,
|
||||||
return $interpreter->addRouteByString(... $closure);
|
'handler' => $controller,
|
||||||
}
|
];
|
||||||
if (!str_contains($closure, '@')) {
|
}
|
||||||
$closure .= '@';
|
return $array;
|
||||||
}
|
}
|
||||||
[$className, $method] = explode('@', $closure);
|
|
||||||
$class = $container->get($this->resetName($className));
|
|
||||||
return $interpreter->addRouteByString($class, $method);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $className
|
* @param string|array $closure
|
||||||
* @return string
|
* @param ControllerInterpreter $interpreter
|
||||||
*/
|
* @return Handler
|
||||||
private function resetName(string $className): string
|
* @throws
|
||||||
{
|
*/
|
||||||
$namespace = array_filter(array_column($this->groupTack, 'namespace'));
|
private function resolve(string|array $closure, ControllerInterpreter $interpreter): Handler
|
||||||
if (count($namespace) < 1) {
|
{
|
||||||
return $className;
|
$container = \Kiri::getDi();
|
||||||
}
|
if (is_array($closure)) {
|
||||||
return implode('\\', $namespace) . '\\' . $className;
|
if (is_string($closure[0])) {
|
||||||
}
|
$closure[0] = $container->get($closure[0]);
|
||||||
|
}
|
||||||
|
$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));
|
||||||
|
$handler = $interpreter->addRouteByString($class, $method);
|
||||||
|
$sourceFile = Router::getCurrentSourceFile();
|
||||||
|
if ($sourceFile !== null) {
|
||||||
|
$handler->setSourceFile($sourceFile);
|
||||||
|
$handler->setSourceKind('route_file');
|
||||||
|
}
|
||||||
|
return $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $path
|
* @param string $className
|
||||||
* @param string $method
|
* @return string
|
||||||
* @param Handler $handler
|
*/
|
||||||
* @return void
|
private function resetName(string $className): string
|
||||||
* @throws
|
{
|
||||||
*/
|
$namespace = array_filter(array_column($this->groupTack, 'namespace'));
|
||||||
public function register(string $path, string $method, Handler $handler): void
|
if (count($namespace) < 1) {
|
||||||
{
|
return $className;
|
||||||
$this->methods[$method . '_' . $path] = $handler;
|
}
|
||||||
$this->registerMiddleware($handler->getClass(), $handler->getMethod());
|
return implode('\\', $namespace) . '\\' . $className;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $class
|
* @param string $path
|
||||||
* @param string $method
|
* @param string $method
|
||||||
* @return void
|
* @param Handler $handler
|
||||||
* @throws
|
* @return void
|
||||||
*/
|
* @throws
|
||||||
public function registerMiddleware(string $class, string $method): void
|
*/
|
||||||
{
|
public function register(string $path, string $method, Handler $handler): void
|
||||||
$middlewares = \request()->middlewares;
|
{
|
||||||
if (count($middlewares) > 0) {
|
if ($handler->getSourceFile() === null && $handler->getClass() !== null) {
|
||||||
$this->appendMiddleware($middlewares, $class, $method);
|
$reflect = \Kiri::getDi()->getReflectionClass($handler->getClass());
|
||||||
}
|
$handler->setSourceFile($this->normalizePath((string)$reflect->getFileName()));
|
||||||
$middlewares = array_column($this->groupTack, 'middleware');
|
$handler->setSourceKind('attribute');
|
||||||
if (count($middlewares) > 0) {
|
}
|
||||||
$this->appendMiddleware($middlewares, $class, $method);
|
|
||||||
}
|
$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
|
||||||
* @param array $middlewares
|
{
|
||||||
* @param $class
|
$entries = [];
|
||||||
* @param $method
|
$hasClosureRoutes = false;
|
||||||
* @return void
|
|
||||||
* @throws
|
|
||||||
*/
|
|
||||||
private function appendMiddleware(array $middlewares, $class, $method): void
|
|
||||||
{
|
|
||||||
foreach ($middlewares as $middleware) {
|
|
||||||
if (is_string($middleware)) {
|
|
||||||
$middleware = [$middleware];
|
|
||||||
}
|
|
||||||
foreach ($middleware as $value) {
|
|
||||||
Middleware::set($class, $method, $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
foreach ($this->methods as $methodPath => $handler) {
|
||||||
* @param string $path
|
if ($handler instanceof Handler && $handler->isClosure()) {
|
||||||
* @param string $method
|
$hasClosureRoutes = true;
|
||||||
* @return HttpRequestHandler
|
continue;
|
||||||
* @throws
|
}
|
||||||
*/
|
|
||||||
public function query(string $path, string $method): HttpRequestHandler
|
[$requestMethod, $path] = explode('_', $methodPath, 2);
|
||||||
{
|
$class = $handler instanceof Handler ? $handler->getClass() : $handler->class;
|
||||||
return $this->httpHandler[$method . '_' . $path] ?? new HttpRequestHandler([], $this->found);
|
$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
|
||||||
* @param string $route
|
{
|
||||||
* @return string
|
if (($artifact['has_closure_routes'] ?? false) === true) {
|
||||||
*/
|
return false;
|
||||||
protected function _splicing_routing(string $route): string
|
}
|
||||||
{
|
|
||||||
$route = ltrim($route, '/');
|
$entries = $artifact['entries'] ?? null;
|
||||||
$prefix = array_column($this->groupTack, 'prefix');
|
if (!is_array($entries)) {
|
||||||
if (empty($prefix = array_filter($prefix))) {
|
return false;
|
||||||
return '/' . $route;
|
}
|
||||||
}
|
|
||||||
return '/' . implode('/', $prefix) . '/' . $route;
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $offset
|
* @param string $class
|
||||||
* @return bool
|
* @param string $method
|
||||||
*/
|
* @return array
|
||||||
public function offsetExists(mixed $offset): bool
|
* @throws ReflectionException
|
||||||
{
|
*/
|
||||||
// TODO: Implement offsetExists() method.
|
public function registerMiddleware(string $class, string $method): array
|
||||||
return isset($this->_item[$offset]);
|
{
|
||||||
}
|
$response = [];
|
||||||
|
$middlewares = \config('servers.request.middlewares', []);
|
||||||
|
if (is_array($middlewares) && count($middlewares) > 0) {
|
||||||
|
$response = $this->appendMiddleware($response, $middlewares);
|
||||||
|
}
|
||||||
|
$middlewares = array_column($this->groupTack, 'middleware');
|
||||||
|
$response = $this->appendMiddleware($response, $middlewares);
|
||||||
|
|
||||||
|
return $this->read_method_middleware($response, $class, $method);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $offset
|
* @param array $response
|
||||||
* @return Router|null
|
* @param string $class
|
||||||
*/
|
* @param string $method
|
||||||
public function offsetGet(mixed $offset): ?Router
|
* @return array
|
||||||
{
|
* @throws ReflectionException
|
||||||
if ($this->offsetExists($offset)) {
|
*/
|
||||||
return $this->_item[$offset];
|
private function read_method_middleware(array $response, string $class, string $method): array
|
||||||
}
|
{
|
||||||
return null;
|
$reflect = \Kiri::getDi()->getReflectionClass($class);
|
||||||
}
|
$attributes = $reflect->getMethod($method)->getAttributes(Annotate\Middleware::class);
|
||||||
|
|
||||||
|
foreach ($attributes as $attribute) {
|
||||||
|
/** @var Annotate\Middleware $instance */
|
||||||
|
$instance = $attribute->newInstance();
|
||||||
|
|
||||||
|
$data = $instance->middleware;
|
||||||
|
if (is_string($data)) {
|
||||||
|
$data = [$data];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($data as $middleware) {
|
||||||
|
if (!in_array($middleware, $response)) {
|
||||||
|
$response[] = $middleware;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $offset
|
* @param array $response
|
||||||
* @param mixed $value
|
* @param array $middlewares
|
||||||
* @return void
|
* @return Defer[]
|
||||||
*/
|
*/
|
||||||
public function offsetSet(mixed $offset, mixed $value): void
|
|
||||||
{
|
private function appendMiddleware(array $response, array $middlewares): array
|
||||||
// TODO: Implement offsetSet() method.
|
{
|
||||||
$this->_item[$offset] = $value;
|
foreach ($middlewares as $middleware) {
|
||||||
}
|
if (is_string($middleware)) {
|
||||||
|
$middleware = [$middleware];
|
||||||
|
}
|
||||||
|
foreach ($middleware as $value) {
|
||||||
|
if (!in_array($value, $response)) {
|
||||||
|
$response[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path
|
||||||
|
* @param string $method
|
||||||
|
* @return HttpRequestHandler
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public function query(string $path, string $method): HttpRequestHandler
|
||||||
|
{
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $offset
|
* @return HttpRequestHandler
|
||||||
* @return void
|
*/
|
||||||
*/
|
protected function not_found_handler(): HttpRequestHandler
|
||||||
public function offsetUnset(mixed $offset): void
|
{
|
||||||
{
|
$middlewares = \config('servers.request.middlewares', []);
|
||||||
unset($this->_item[$offset]);
|
if (!is_array($middlewares) || count($middlewares) < 1) {
|
||||||
}
|
return new HttpRequestHandler($middlewares, $this->found);
|
||||||
|
}
|
||||||
|
for ($index = 0; $index < count($middlewares); $index++) {
|
||||||
|
$middlewares[$index] = \Kiri::getDi()->get($middlewares[$index]);
|
||||||
|
}
|
||||||
|
return new HttpRequestHandler($middlewares, $this->found);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function warmHttpHandlers(): void
|
||||||
|
{
|
||||||
|
foreach ($this->methods as $name => $method) {
|
||||||
|
$this->httpHandler[$name] = $this->compileHandler($method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $route
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function _splicing_routing(string $route): string
|
||||||
|
{
|
||||||
|
$route = ltrim($route, '/');
|
||||||
|
$prefix = array_column($this->groupTack, 'prefix');
|
||||||
|
if (empty($prefix = array_filter($prefix))) {
|
||||||
|
return '/' . $route;
|
||||||
|
}
|
||||||
|
return '/' . implode('/', $prefix) . '/' . $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
public function offsetExists(mixed $offset): bool
|
||||||
|
{
|
||||||
|
// TODO: Implement offsetExists() method.
|
||||||
|
return isset($this->_item[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $offset
|
||||||
|
* @return Router|null
|
||||||
|
*/
|
||||||
|
public function offsetGet(mixed $offset): ?Router
|
||||||
|
{
|
||||||
|
if ($this->offsetExists($offset)) {
|
||||||
|
return $this->_item[$offset];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $offset
|
||||||
|
* @param mixed $value
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function offsetSet(mixed $offset, mixed $value): void
|
||||||
|
{
|
||||||
|
// TODO: Implement offsetSet() method.
|
||||||
|
$this->_item[$offset] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $offset
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function offsetUnset(mixed $offset): void
|
||||||
|
{
|
||||||
|
unset($this->_item[$offset]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+415
@@ -0,0 +1,415 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Kiri\Router;
|
||||||
|
|
||||||
|
use Kiri\Di\Context;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Session
|
||||||
|
* @package Kiri\Router
|
||||||
|
*/
|
||||||
|
class Session
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Session 数据存储键
|
||||||
|
*/
|
||||||
|
private const string SESSION_KEY = 'kiri.session.data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session ID 键
|
||||||
|
*/
|
||||||
|
private const string SESSION_ID_KEY = 'kiri.session.id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session 名称
|
||||||
|
*/
|
||||||
|
private const string SESSION_NAME = 'KIRI_SESSION_ID';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session 存储路径
|
||||||
|
*/
|
||||||
|
private static ?string $savePath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session 生命周期(秒)
|
||||||
|
*/
|
||||||
|
private static int $lifetime = 7200; // 默认2小时
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已启动
|
||||||
|
*/
|
||||||
|
private static bool $started = false;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Session
|
||||||
|
* @param string|null $savePath Session 存储路径
|
||||||
|
* @param int $lifetime Session 生命周期(秒)
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(?string $savePath = null, int $lifetime = 7200): void
|
||||||
|
{
|
||||||
|
self::$savePath = $savePath ?? storage(null, 'session');
|
||||||
|
self::$lifetime = $lifetime;
|
||||||
|
|
||||||
|
// 确保存储目录存在
|
||||||
|
if (!is_dir(self::$savePath)) {
|
||||||
|
mkdir(self::$savePath, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 Session
|
||||||
|
* @param ServerRequestInterface|null $request
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function start(?ServerRequestInterface $request = null): void
|
||||||
|
{
|
||||||
|
if (self::$started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request === null) {
|
||||||
|
$request = \request();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取或创建 Session ID
|
||||||
|
$sessionId = self::getSessionId($request);
|
||||||
|
|
||||||
|
// 加载 Session 数据
|
||||||
|
$sessionData = self::loadSessionData($sessionId);
|
||||||
|
|
||||||
|
// 存储到 Context
|
||||||
|
Context::set(self::SESSION_KEY, $sessionData);
|
||||||
|
Context::set(self::SESSION_ID_KEY, $sessionId);
|
||||||
|
|
||||||
|
self::$started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Session ID
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function getSessionId(ServerRequestInterface $request): string
|
||||||
|
{
|
||||||
|
$cookies = $request->cookieParams;
|
||||||
|
|
||||||
|
// 从 Cookie 中获取 Session ID
|
||||||
|
if (isset($cookies[self::SESSION_NAME]) && !empty($cookies[self::SESSION_NAME])) {
|
||||||
|
$sessionId = $cookies[self::SESSION_NAME];
|
||||||
|
// 验证 Session ID 格式
|
||||||
|
if (preg_match('/^[a-zA-Z0-9]{32,}$/', $sessionId)) {
|
||||||
|
return $sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成新的 Session ID
|
||||||
|
return self::generateSessionId();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Session ID
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function generateSessionId(): string
|
||||||
|
{
|
||||||
|
return bin2hex(random_bytes(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 Session 数据
|
||||||
|
* @param string $sessionId
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadSessionData(string $sessionId): array
|
||||||
|
{
|
||||||
|
$filePath = self::getSessionFilePath($sessionId);
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = file_get_contents($filePath);
|
||||||
|
if ($data === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionData = json_decode($data, true);
|
||||||
|
if (!is_array($sessionData)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (isset($sessionData['expires_at']) && $sessionData['expires_at'] < time()) {
|
||||||
|
self::destroySession($sessionId);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sessionData['data'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Session 数据
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function save(): void
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = Context::get(self::SESSION_ID_KEY);
|
||||||
|
$sessionData = Context::get(self::SESSION_KEY);
|
||||||
|
|
||||||
|
if ($sessionId === null || $sessionData === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = self::getSessionFilePath($sessionId);
|
||||||
|
$data = [
|
||||||
|
'data' => $sessionData,
|
||||||
|
'expires_at' => time() + self::$lifetime,
|
||||||
|
'created_at' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents($filePath, json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
|
// 设置 Cookie
|
||||||
|
self::setSessionCookie($sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Session Cookie
|
||||||
|
* @param string $sessionId
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
private static function setSessionCookie(string $sessionId): void
|
||||||
|
{
|
||||||
|
$response = \response();
|
||||||
|
$expires = time() + self::$lifetime;
|
||||||
|
|
||||||
|
// 获取现有的 Cookie 参数
|
||||||
|
$cookieParams = $response->getCookieParams();
|
||||||
|
|
||||||
|
// 添加 Session Cookie
|
||||||
|
// Swoole setCookie 参数: name, value, expires, path, domain, secure, httponly, samesite
|
||||||
|
$cookieParams[] = [
|
||||||
|
self::SESSION_NAME, // name
|
||||||
|
$sessionId, // value
|
||||||
|
$expires, // expires
|
||||||
|
'/', // path
|
||||||
|
'', // domain
|
||||||
|
false, // secure
|
||||||
|
true, // httponly
|
||||||
|
'Lax' // samesite
|
||||||
|
];
|
||||||
|
|
||||||
|
$response->withCookieParams($cookieParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Session 文件路径
|
||||||
|
* @param string $sessionId
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function getSessionFilePath(string $sessionId): string
|
||||||
|
{
|
||||||
|
if (self::$savePath === null) {
|
||||||
|
self::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$savePath . '/' . $sessionId . '.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁 Session 文件
|
||||||
|
* @param string $sessionId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function destroySession(string $sessionId): void
|
||||||
|
{
|
||||||
|
$filePath = self::getSessionFilePath($sessionId);
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储 Session 数据
|
||||||
|
* @param string|array $key 键名或键值对数组
|
||||||
|
* @param mixed $value 值(当 $key 为字符串时)
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function put(string|array $key, mixed $value = null): void
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
self::start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionData = Context::get(self::SESSION_KEY, []);
|
||||||
|
|
||||||
|
// 支持批量设置
|
||||||
|
if (is_array($key)) {
|
||||||
|
$sessionData = array_merge($sessionData, $key);
|
||||||
|
} else {
|
||||||
|
$sessionData[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Context::set(self::SESSION_KEY, $sessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Session 数据
|
||||||
|
* @param string|null $key 键名,为 null 时返回所有数据
|
||||||
|
* @param mixed $default 默认值
|
||||||
|
* @return mixed
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function get(?string $key = null, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
self::start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionData = Context::get(self::SESSION_KEY, []);
|
||||||
|
|
||||||
|
// 如果 key 为 null,返回所有数据
|
||||||
|
if ($key === null) {
|
||||||
|
return $sessionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sessionData[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 Session 中是否存在指定键
|
||||||
|
* @param string $key
|
||||||
|
* @return bool
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function has(string $key): bool
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
self::start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionData = Context::get(self::SESSION_KEY, []);
|
||||||
|
return isset($sessionData[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 Session 数据
|
||||||
|
* @param string $key
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function forget(string $key): void
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
self::start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionData = Context::get(self::SESSION_KEY, []);
|
||||||
|
unset($sessionData[$key]);
|
||||||
|
Context::set(self::SESSION_KEY, $sessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有 Session 数据
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function flush(): void
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
self::start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Context::set(self::SESSION_KEY, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有 Session 数据
|
||||||
|
* @return array
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function all(): array
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
self::start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Context::get(self::SESSION_KEY, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁 Session
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function destroy(): void
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = Context::get(self::SESSION_ID_KEY);
|
||||||
|
if ($sessionId !== null) {
|
||||||
|
self::destroySession($sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Context::set(self::SESSION_KEY, []);
|
||||||
|
Context::set(self::SESSION_ID_KEY, null);
|
||||||
|
self::$started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成 Session ID
|
||||||
|
* @return void
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public static function regenerate(): void
|
||||||
|
{
|
||||||
|
if (!self::$started) {
|
||||||
|
self::start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldSessionId = Context::get(self::SESSION_ID_KEY);
|
||||||
|
|
||||||
|
// 生成新的 Session ID
|
||||||
|
$newSessionId = self::generateSessionId();
|
||||||
|
|
||||||
|
// 删除旧 Session 文件
|
||||||
|
if ($oldSessionId !== null) {
|
||||||
|
self::destroySession($oldSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存新 Session
|
||||||
|
Context::set(self::SESSION_ID_KEY, $newSessionId);
|
||||||
|
self::setSessionCookie($newSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,16 +3,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Kiri\Router;
|
namespace Kiri\Router;
|
||||||
|
|
||||||
use Kiri\Di\Inject\Container;
|
|
||||||
use Kiri\Di\Interface\ResponseEmitterInterface;
|
use Kiri\Di\Interface\ResponseEmitterInterface;
|
||||||
use Kiri\Events\EventDispatch;
|
use Kiri\Events\EventDispatch;
|
||||||
use Kiri\Events\EventProvider;
|
use Kiri\Events\EventProvider;
|
||||||
use Kiri\Server\Events\OnAfterRequest;
|
use Kiri\Server\Events\OnAfterRequest;
|
||||||
use Psr\Container\ContainerExceptionInterface;
|
|
||||||
use Psr\Container\NotFoundExceptionInterface;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use ReflectionException;
|
|
||||||
use SplPriorityQueue;
|
use SplPriorityQueue;
|
||||||
|
use function swoole_version;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +66,7 @@ class SwooleHttpResponseEmitter implements ResponseEmitterInterface
|
|||||||
*/
|
*/
|
||||||
private function writeParams(ResponseInterface $proxy, object $response, object $request): void
|
private function writeParams(ResponseInterface $proxy, object $response, object $request): void
|
||||||
{
|
{
|
||||||
|
/** @var \Swoole\Http\Response $response */
|
||||||
$response->setStatusCode($proxy->getStatusCode());
|
$response->setStatusCode($proxy->getStatusCode());
|
||||||
$headers = $proxy->getHeaders();
|
$headers = $proxy->getHeaders();
|
||||||
if (count($headers) > 0) foreach ($headers as $name => $header) {
|
if (count($headers) > 0) foreach ($headers as $name => $header) {
|
||||||
@@ -78,9 +76,9 @@ class SwooleHttpResponseEmitter implements ResponseEmitterInterface
|
|||||||
if (count($cookieParams) > 0) foreach ($cookieParams as $cookie) {
|
if (count($cookieParams) > 0) foreach ($cookieParams as $cookie) {
|
||||||
$response->setCookie(...$cookie);
|
$response->setCookie(...$cookie);
|
||||||
}
|
}
|
||||||
$response->header('Run-Time', $this->getRunTime($request));
|
$response->header('Run-Time', $this->getRunTime($request) . '');
|
||||||
$response->header('Server', 'swoole');
|
$response->header('Server', 'swoole');
|
||||||
$response->header('Swoole-Version', \swoole_version());
|
$response->header('Swoole-Version', swoole_version());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class MixedProxy extends TypesProxy
|
|||||||
try {
|
try {
|
||||||
return $value == ($form->{$field} = $value);
|
return $value == ($form->{$field} = $value);
|
||||||
} catch (\Throwable $throwable) {
|
} catch (\Throwable $throwable) {
|
||||||
return false;
|
return $this->getLogger()->json_log($throwable, [], false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace Kiri\Router\Validator\Types;
|
namespace Kiri\Router\Validator\Types;
|
||||||
|
|
||||||
abstract class TypesProxy
|
use Kiri\Abstracts\Component;
|
||||||
|
|
||||||
|
abstract class TypesProxy extends Component
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -133,11 +133,9 @@ class Validator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @var array $rule */
|
/** @var array $rule */
|
||||||
foreach ($rules as $rule) {
|
if (array_any($rules, fn($rule) => !call_user_func($rule, $this->formData->{$name}))) {
|
||||||
if (!call_user_func($rule, $this->formData->{$name})) {
|
return $this->addError('Request field ' . $name . ' value format error');
|
||||||
return $this->addError('Request field ' . $name . ' value format error');
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ class ValidatorMiddleware implements MiddlewareInterface
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $class
|
* @param ResponseInterface $response
|
||||||
* @param string $method
|
* @param string $class
|
||||||
*/
|
* @param string $method
|
||||||
public function __construct(public string $class, public string $method)
|
*/
|
||||||
|
public function __construct(public ResponseInterface $response ,public string $class, public string $method)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ class ValidatorMiddleware implements MiddlewareInterface
|
|||||||
if (!$validator->run($request)) {
|
if (!$validator->run($request)) {
|
||||||
Kiri::getLogger()->println($request->getUri()->getPath() . ' `' . $validator->error() . '`');
|
Kiri::getLogger()->println($request->getUri()->getPath() . ' `' . $validator->error() . '`');
|
||||||
|
|
||||||
return di(ResponseInterface::class)->html($validator->error(), 415);
|
return $this->response->html($validator->error(), 400);
|
||||||
} else {
|
} else {
|
||||||
return $handler->handle($request);
|
return $handler->handle($request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Kiri\Router\Blade\BladeFactory;
|
||||||
|
use Kiri\Router\Blade\BladeHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染 Blade 视图
|
||||||
|
*
|
||||||
|
* @param string $path 视图路径(支持 . 分隔,如 'user.profile')
|
||||||
|
* @param array $data 视图数据
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
function View(string $path, array $data = []): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = \response();
|
||||||
|
$response->withAddedHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取视图路径和缓存路径
|
||||||
|
$viewPath = APP_PATH . 'resources/view';
|
||||||
|
$cachePath = storage(null, 'view/cache');
|
||||||
|
|
||||||
|
// 创建或获取 BladeFactory 实例
|
||||||
|
$factory = BladeHelper::getFactory();
|
||||||
|
if ($factory->getViewPath() !== $viewPath) {
|
||||||
|
$factory = new BladeFactory($viewPath, $cachePath);
|
||||||
|
BladeHelper::setFactory($factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染视图
|
||||||
|
return $response->html($factory->render($path, $data));
|
||||||
|
} catch (\Exception $throwable) {
|
||||||
|
\Kiri::getLogger()->json_log($throwable);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
extract(['errorData' => $throwable], EXTR_SKIP);
|
||||||
|
include __DIR__.'/template/error.php';
|
||||||
|
|
||||||
|
$message = ob_get_clean();
|
||||||
|
|
||||||
|
return $response->html($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<meta charset='UTF-8'>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
|
<title>Error: <?= htmlspecialchars($errorData['type']) ?></title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-header {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title i {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header:hover {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-toggle {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background: #282c34;
|
||||||
|
color: #abb2bf;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path {
|
||||||
|
color: #61afef;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
color: #98c379;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #e06c75;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-header {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-location {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-error {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-info {
|
||||||
|
background: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #5a6fd8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.error-container {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-info {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-header">
|
||||||
|
<h1 class="error-title">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<?= htmlspecialchars($errorData['type']) ?>
|
||||||
|
</h1>
|
||||||
|
<div class="error-subtitle">
|
||||||
|
<?= htmlspecialchars($errorData['message']) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-content">
|
||||||
|
<!-- Error Details -->
|
||||||
|
<div class="error-section">
|
||||||
|
<div class="section-header" onclick="toggleSection('details')">
|
||||||
|
<span class="section-title">Error Details</span>
|
||||||
|
<span class="section-toggle" id="toggle-details">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content" id="section-details">
|
||||||
|
<div class="code-block">
|
||||||
|
<span class="file-path"><?= htmlspecialchars($errorData['file']) ?></span>
|
||||||
|
<span class="line-number">:<?= $errorData['line'] ?></span>
|
||||||
|
<br><br>
|
||||||
|
<span class="error-message"><?= htmlspecialchars($errorData['message']) ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<td><span class="badge badge-error"><?= htmlspecialchars($errorData['type']) ?></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<td><?= $errorData['code'] ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<td><?= $errorData['timestamp'] ?></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stack Trace -->
|
||||||
|
<div class="error-section">
|
||||||
|
<div class="section-header" onclick="toggleSection('trace')">
|
||||||
|
<span class="section-title">Stack Trace</span>
|
||||||
|
<span class="section-toggle" id="toggle-trace">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content" id="section-trace">
|
||||||
|
<?php
|
||||||
|
foreach ($errorData['trace'] as $index => $trace): ?>
|
||||||
|
<div class="trace-item">
|
||||||
|
<div class="trace-header">
|
||||||
|
#<?= $index ?>:
|
||||||
|
<?= $trace['class'] ?? '' ?><?= $trace['type'] ?? '' ?><?= $trace['function'] ?>()
|
||||||
|
</div>
|
||||||
|
<div class="trace-location">
|
||||||
|
<?= $trace['file'] ?? 'internal' ?><?= isset($trace['line']) ? ':' . $trace['line'] : '' ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
if (!empty($trace['args'])): ?>
|
||||||
|
<div style="margin-top: 5px; font-size: 12px; color: #666;">
|
||||||
|
Arguments: <?= implode(', ', $trace['args']) ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Information -->
|
||||||
|
<div class="error-section">
|
||||||
|
<div class="section-header" onclick="toggleSection('server')">
|
||||||
|
<span class="section-title">Server Information</span>
|
||||||
|
<span class="section-toggle" id="toggle-server">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content hidden" id="section-server">
|
||||||
|
<table>
|
||||||
|
<?php
|
||||||
|
foreach ($errorData['server'] as $key => $value): ?>
|
||||||
|
<tr>
|
||||||
|
<th><?= htmlspecialchars($key) ?></th>
|
||||||
|
<td><?= htmlspecialchars($value) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Request Information -->
|
||||||
|
<div class="error-section">
|
||||||
|
<div class="section-header" onclick="toggleSection('request')">
|
||||||
|
<span class="section-title">Request Information</span>
|
||||||
|
<span class="section-toggle" id="toggle-request">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content hidden" id="section-request">
|
||||||
|
<h4>GET Parameters</h4>
|
||||||
|
<?php
|
||||||
|
if (!empty($errorData['request']['GET'])): ?>
|
||||||
|
<table>
|
||||||
|
<?php
|
||||||
|
foreach ($errorData['request']['GET'] as $key => $value): ?>
|
||||||
|
<tr>
|
||||||
|
<th><?= htmlspecialchars($key) ?></th>
|
||||||
|
<td><?= htmlspecialchars(is_array($value) ? json_encode($value) : $value) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
endforeach; ?>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
else: ?>
|
||||||
|
<p>No GET parameters</p>
|
||||||
|
<?php
|
||||||
|
endif; ?>
|
||||||
|
|
||||||
|
<h4>POST Parameters</h4>
|
||||||
|
<?php
|
||||||
|
if (!empty($errorData['request']['POST'])): ?>
|
||||||
|
<table>
|
||||||
|
<?php
|
||||||
|
foreach ($errorData['request']['POST'] as $key => $value): ?>
|
||||||
|
<tr>
|
||||||
|
<th><?= htmlspecialchars($key) ?></th>
|
||||||
|
<td><?= htmlspecialchars(is_array($value) ? json_encode($value) : $value) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
endforeach; ?>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
else: ?>
|
||||||
|
<p>No POST parameters</p>
|
||||||
|
<?php
|
||||||
|
endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Environment Info -->
|
||||||
|
<div class="environment-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Memory Usage</div>
|
||||||
|
<div class="info-value"><?= $errorData['memory_usage'] ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Peak Memory</div>
|
||||||
|
<div class="info-value"><?= $errorData['peak_memory'] ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Execution Time</div>
|
||||||
|
<div class="info-value"><?= round($errorData['execution_time'], 4) ?> seconds</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">PHP Version</div>
|
||||||
|
<div class="info-value"><?= PHP_VERSION ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-primary" onclick="window.location.reload()">
|
||||||
|
<i class="fas fa-redo"></i> Reload Page
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="window.history.back()">
|
||||||
|
<i class="fas fa-arrow-left"></i> Go Back
|
||||||
|
</button>
|
||||||
|
<a href="/" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-home"></i> Go Home
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-secondary" onclick="copyErrorDetails()">
|
||||||
|
<i class="fas fa-copy"></i> Copy Error Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleSection(sectionId) {
|
||||||
|
const section = document.getElementById(`section-${sectionId}`);
|
||||||
|
const toggle = document.getElementById(`toggle-${sectionId}`);
|
||||||
|
|
||||||
|
if (section.classList.contains('hidden')) {
|
||||||
|
section.classList.remove('hidden');
|
||||||
|
toggle.textContent = '▲';
|
||||||
|
} else {
|
||||||
|
section.classList.add('hidden');
|
||||||
|
toggle.textContent = '▼';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyErrorDetails() {
|
||||||
|
const errorDetails = {
|
||||||
|
type: '<?= addslashes($errorData['type']) ?>',
|
||||||
|
message: '<?= addslashes($errorData['message']) ?>',
|
||||||
|
file: '<?= addslashes($errorData['file']) ?>',
|
||||||
|
line: <?= $errorData['line'] ?>,
|
||||||
|
timestamp: '<?= $errorData['timestamp'] ?>',
|
||||||
|
trace: <?= json_encode($errorData['trace']) ?>
|
||||||
|
};
|
||||||
|
|
||||||
|
const text = `Error Details:
|
||||||
|
Type: ${errorDetails.type}
|
||||||
|
Message: ${errorDetails.message}
|
||||||
|
File: ${errorDetails.file}
|
||||||
|
Line: ${errorDetails.line}
|
||||||
|
Timestamp: ${errorDetails.timestamp}
|
||||||
|
|
||||||
|
Stack Trace:
|
||||||
|
${JSON.stringify(errorDetails.trace, null, 2)}`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
alert('Error details copied to clipboard!');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
alert('Failed to copy error details');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-expand first section
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
toggleSection('details');
|
||||||
|
toggleSection('trace');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user