86 Commits

Author SHA1 Message Date
as2252258 28973df8b6 eee 2026-06-12 23:57:20 +08:00
as2252258 4cfd04c988 eee 2026-04-17 16:30:52 +08:00
as2252258 16b8df159a eee 2026-04-17 13:56:30 +08:00
as2252258 83f6dc721a eee 2026-04-17 11:54:24 +08:00
as2252258 a2138bdd3b eee 2026-02-26 14:39:05 +08:00
as2252258 d098b293a4 Merge remote-tracking branch 'origin/main' 2026-01-02 01:05:03 +08:00
as2252258 f2f2ee408c eee 2026-01-02 01:04:50 +08:00
as2252258 76d2d2c64e xxx
Signed-off-by: 向林 <as2252258@163.com>
2025-12-31 06:47:28 +00:00
as2252258 8778cd220f ea
Signed-off-by: 向林 <as2252258@163.com>
2025-12-31 06:46:30 +00:00
as2252258 6929bed90a eee 2025-12-31 01:51:47 +08:00
as2252258 2f08fad1ef eee 2025-12-31 01:07:48 +08:00
as2252258 91e0fe8940 eee 2025-12-31 00:19:29 +08:00
as2252258 34051feb87 eee 2025-12-30 21:55:08 +08:00
as2252258 d32fb67650 eee 2025-12-30 21:54:08 +08:00
as2252258 b28b45b15e eee 2025-12-30 21:53:16 +08:00
as2252258 371e11baa9 eee 2025-12-30 21:52:06 +08:00
as2252258 5358205fbe eee 2025-12-30 21:50:02 +08:00
as2252258 f4430e2dd0 eee 2025-12-30 21:49:20 +08:00
as2252258 3a41eaa95c eee 2025-12-30 21:48:31 +08:00
as2252258 6cb95f4749 eee 2025-12-30 21:46:56 +08:00
as2252258 299255ce7a eee 2025-12-30 21:46:22 +08:00
as2252258 51587768cf eee 2025-12-30 21:44:43 +08:00
as2252258 8165b61f72 eee 2025-12-30 21:43:56 +08:00
as2252258 0c17482b20 eee 2025-12-30 21:43:32 +08:00
as2252258 4c9f33a6dc eee 2025-12-30 21:41:32 +08:00
as2252258 42f945c195 eee 2025-12-30 21:40:06 +08:00
as2252258 a11e6a3eee eee 2025-12-30 21:39:18 +08:00
as2252258 dfa50c9448 eee 2025-12-30 21:37:37 +08:00
as2252258 ad56abe39d eee 2025-12-30 21:21:25 +08:00
as2252258 c148c6135a eee 2025-12-30 21:19:22 +08:00
as2252258 24cb6e2b5b eee 2025-12-30 21:15:57 +08:00
as2252258 443df009e1 eee 2025-12-30 21:15:25 +08:00
as2252258 7b9475a1c9 eee 2025-12-30 21:11:44 +08:00
as2252258 67bd83933e eee 2025-12-30 21:09:14 +08:00
as2252258 1f4916f661 eee 2025-12-30 21:08:08 +08:00
as2252258 44f8bc6535 eee 2025-12-30 21:07:31 +08:00
as2252258 30cc98c00c eee 2025-12-30 20:28:15 +08:00
as2252258 9968ea778e eee 2025-12-30 20:26:05 +08:00
as2252258 870e9b530e eee 2025-12-30 20:25:47 +08:00
as2252258 669b8ed49f eee 2025-12-30 20:23:33 +08:00
as2252258 d626d5bed7 eee 2025-12-30 20:23:06 +08:00
as2252258 ea3eacdb84 eee 2025-12-30 20:21:44 +08:00
as2252258 01ad7d7416 eee 2025-12-30 20:19:15 +08:00
as2252258 58cb9b53db eee 2025-12-30 20:17:46 +08:00
as2252258 c243348e78 eee 2025-12-30 20:09:05 +08:00
as2252258 05d3110522 eee 2025-12-30 20:03:40 +08:00
as2252258 1b08ae2be3 eee 2025-12-30 20:00:50 +08:00
as2252258 45ed584435 eee 2025-12-30 19:28:04 +08:00
as2252258 5ba0f45528 eee 2025-12-30 19:23:21 +08:00
as2252258 791267c26f eee 2025-12-30 19:21:36 +08:00
as2252258 8d53079ba4 eee 2025-12-30 19:20:40 +08:00
as2252258 2d42ed7090 eee 2025-12-30 19:08:10 +08:00
as2252258 99af7df9fb eee 2025-12-30 19:05:57 +08:00
as2252258 6990ef5bcc eee 2025-12-30 19:04:18 +08:00
as2252258 705e916bda a
Signed-off-by: 向林 <as2252258@163.com>
2025-12-30 09:48:53 +00:00
as2252258 92dd57429d a
Signed-off-by: 向林 <as2252258@163.com>
2025-12-30 09:22:00 +00:00
as2252258 975e7a3cd0 eee 2025-12-18 15:39:44 +08:00
as2252258 15d54e8ffe eee 2025-12-18 14:47:16 +08:00
as2252258 7b1767ab5f eee 2025-12-17 21:12:25 +08:00
as2252258 804b9bd67f eee 2025-12-17 21:11:49 +08:00
as2252258 153667f0b4 eee 2025-12-01 07:16:58 +08:00
as2252258 89ac36c227 eee 2025-12-01 06:39:04 +08:00
as2252258 79ce7142d9 ea 2025-12-01 06:19:25 +08:00
as2252258 90ee572092 ea 2025-11-26 05:09:36 +08:00
as2252258 926cbea0b9 eee 2025-07-21 17:26:51 +08:00
as2252258 3251045a5b eee 2025-07-16 09:09:48 +08:00
as2252258 5b8163b79e eee 2025-07-14 18:41:52 +08:00
as2252258 be99f4dcbb eee 2025-07-14 18:39:11 +08:00
as2252258 cd79309db3 eee 2025-07-14 15:35:26 +08:00
as2252258 0c2462feee eee 2025-07-10 10:30:20 +08:00
as2252258 4c4a21dd7a eee 2025-07-10 09:30:13 +08:00
as2252258 8623a036ed eee 2025-07-08 11:43:04 +08:00
as2252258 f38942f4f3 eee 2024-12-16 16:36:35 +08:00
as2252258 dc561cec9b eee 2024-12-16 16:29:35 +08:00
as2252258 daa02a6408 eee 2024-12-16 16:05:01 +08:00
as2252258 34ab8f145c eee 2024-12-16 15:55:29 +08:00
as2252258 ae20755bd7 eee 2024-12-16 15:47:00 +08:00
as2252258 011e95a3f2 eee 2024-12-16 15:44:56 +08:00
as2252258 5bda66b40d eee 2024-11-18 17:05:21 +08:00
as2252258 e82fad2fcb eee 2024-11-18 17:02:58 +08:00
as2252258 16eb6b11c5 eee 2024-11-18 16:11:16 +08:00
as2252258 227b6fa512 eee 2024-11-18 16:09:59 +08:00
as2252258 c06ab29054 eee 2024-11-18 14:21:43 +08:00
as2252258 3ccf08fdfb eee 2024-11-15 14:24:57 +08:00
as2252258 292ccc84de eee 2024-11-15 14:18:20 +08:00
as2252258 edc7371d9b eee 2024-11-15 14:16:37 +08:00
48 changed files with 6623 additions and 3008 deletions
+80
View File
@@ -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
View File
@@ -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"
]
} }
} }
+8
View File
@@ -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>
+42
View File
@@ -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>&copy; {{ date('Y') }} Kiri Framework. 使用 Blade 模板引擎构建。</p>
</footer>
</body>
</html>
+53
View File
@@ -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";
}
+54
View File
@@ -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
+1 -1
View File
@@ -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);
} }
+41 -23
View File
@@ -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;
}
} }
+12 -1
View File
@@ -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
View File
@@ -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;
} }
+7 -4
View File
@@ -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);
} }
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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.");
} }
+39
View File
@@ -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;
}
}
+671
View File
@@ -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);
}
}
+144
View File
@@ -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;
}
}
+58
View File
@@ -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);
}
}
+93
View File
@@ -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;
}
}
+252
View File
@@ -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>&copy; 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. 建议在生产环境中定期清理缓存目录
+405
View File
@@ -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>
```
File diff suppressed because it is too large Load Diff
+20
View File
@@ -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
+25 -50
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -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();
}
}
} }
+18 -8
View File
@@ -9,14 +9,24 @@ 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
{
return $this->response->withBody(new Stream(json_encode($result, JSON_UNESCAPED_UNICODE)));
}
} }
+13 -5
View File
@@ -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,13 @@ 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))); return $this->response->withBody(new Stream(json_encode($result, JSON_UNESCAPED_UNICODE)));
} else { } else {
return $response->withBody(new Stream((string)$result)); return $this->response->withBody(new Stream((string)$result));
} }
} }
+32 -23
View File
@@ -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));
}
}
} }
+7 -2
View File
@@ -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));
} }
+18 -9
View File
@@ -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;
}
} }
+104 -1
View File
@@ -5,6 +5,7 @@ namespace Kiri\Router;
use Closure; use Closure;
use Kiri; use Kiri;
use Kiri\Router\Annotate\Defer;
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 +28,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 +64,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 +115,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 +188,35 @@ 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);
} }
/**
* @return void
*/
private function executeDeferred(): void
{
foreach ($this->deferred as $defer) {
try {
$callback = $defer->callback;
$params = $defer->params;
if (is_array($callback)) {
[$class, $method] = $callback;
$instance = Kiri::getDi()->get($class);
call_user_func([$instance, $method], ...$params);
} else {
$instance = Kiri::getDi()->get($callback);
call_user_func([$instance, '__invoke'], ...$params);
}
} catch (\Throwable $throwable) {
\Kiri::getLogger()->error('Defer callback failed: ' . $throwable->getMessage());
}
}
}
} }
-1
View File
@@ -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
{ {
+102
View File
@@ -0,0 +1,102 @@
<?php
use PhpParser\Error;
use PhpParser\Node;
use PhpParser\NodeDumper;
use PhpParser\NodeFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter\Standard;
use Psr\Http\Message\ResponseInterface;
class MiddlewareProxyGenerator {
private static string $cacheFile = __DIR__ . '/middleware_proxy.php';
private static NodeFactory $factory;
public static function generateProxy(array $middlewares): string {
self::$factory = new NodeFactory();
// 创建类结构(v5.5.0兼容写法)
$class = new Node\Stmt\Class_(
'MiddlewareProxy',
[
'implements' => [new Node\Name\FullyQualified(\Psr\Http\Server\MiddlewareInterface::class)],
'stmts' => []
]
);
// 生成中间件调用链
$method = self::createMethodNode($middlewares);
$class->stmts[] = $method;
// 生成代码
$printer = new Standard();
return $printer->prettyPrintFile([$class]);
}
private static function createMethodNode(array $middlewares): Node\Stmt\Function_ {
$method = new Node\Stmt\Function_(
'process',
[
'params' => [
new Node\Param(
new Node\Expr\Variable('request'),
null,
new Node\UnionType([new Node\Expr\Variable(\Psr\Http\Message\ServerRequestInterface::class)])
),
new Node\Param(
new Node\Expr\Variable('handler'),
null,
new Node\UnionType([new Node\Expr\Variable(\Psr\Http\Server\RequestHandlerInterface::class)])
)
],
'returnType' => new Node\Name(ResponseInterface::class),
'stmts' => []
]
);
// 生成中间件调用链(v5.5.0闭包写法)
$chain = [];
$count = count($middlewares);
for ($i = 0; $i < $count; $i++) {
$className = $middlewares[$i]::class;
$chain[] = self::createMiddlewareCall($i, $className, $count);
}
$method->stmts = array_reverse($chain);
return $method;
}
private static function createMiddlewareCall(int $index, string $className, int $count): Node {
$mwVar = new Node\Expr\Variable("mw{$index}");
$nextHandler = new Node\Expr\Variable("next");
// 创建中间件实例(v5.5.0兼容)
$instanciate = new Node\Expr\New_($className);
// 创建闭包表达式(v5.5.0语法)
$closure = new Node\Expr\Closure([
'static' => true,
'params' => [new Node\Param($nextHandler)],
'stmts' => [
new Node\Stmt\Expression(
new Node\Expr\MethodCall(
$instanciate,
'process',
[new Node\Arg($nextHandler), new Node\Arg($mwVar)]
)
)
]
]);
return new Node\Stmt\Expression($closure);
}
}
// 生成代理类代码
$middlewares = [AuthMiddleware::class, LogMiddleware::class];
$proxyCode = MiddlewareProxyGenerator::generateProxy($middlewares);
// 写入代理类文件
file_put_contents(MiddlewareProxyGenerator::$cacheFile, $proxyCode);
+25 -13
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Kiri\Router; namespace Kiri\Router;
use Kiri\Abstracts\CoordinatorManager;
use Kiri\Coordinator;
use Kiri\Di\Inject\Container; use Kiri\Di\Inject\Container;
use Kiri\Di\Context; use Kiri\Di\Context;
use Kiri\Di\Interface\ResponseEmitterInterface; use Kiri\Di\Interface\ResponseEmitterInterface;
@@ -15,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;
@@ -31,6 +34,9 @@ class OnRequest implements OnRequestInterface
public RouterCollector $router; public RouterCollector $router;
public DataGrip $dataGrip;
/** /**
* @var ExceptionHandlerInterface * @var ExceptionHandlerInterface
*/ */
@@ -56,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;
} }
@@ -73,13 +80,19 @@ class OnRequest implements OnRequestInterface
*/ */
public function onRequest(Request $request, Response $response): void public function onRequest(Request $request, Response $response): void
{ {
/** @var CQ $PsrRequest */
try { try {
$PsrRequest = $this->initRequestAndResponse($request); $this->setResponseHeaders($response, $this->response->headers);
$PsrResponse = $this->router->query($request->server['path_info'], $request->getMethod()) /** @var CQ $PsrRequest */
->run($PsrRequest); Context::set(ResponseInterface::class, new ConstrictResponse($this->response->contentType));
$PsrRequest = Context::set(RequestInterface::class, CQ::builder($request));
$this->router = $this->dataGrip->get(ROUTER_TYPE_HTTP);
CoordinatorManager::utility(Coordinator::WORKER_START)->yield();
$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);
@@ -88,16 +101,15 @@ class OnRequest implements OnRequestInterface
/** /**
* @param Request $request * @param Response $response
* @return ServerRequestInterface * @param array $headers
* @return void
*/ */
public function initRequestAndResponse(Request $request): ServerRequestInterface protected function setResponseHeaders(Response $response, array $headers): void
{ {
$response = new ConstrictResponse($this->response->contentType); foreach ($headers as $key => $header) {
$response->header($key, $header);
Context::set(ResponseInterface::class, $response); }
return Context::set(RequestInterface::class, CQ::builder($request));
} }
} }
+839 -829
View File
File diff suppressed because it is too large Load Diff
+419 -381
View File
@@ -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__);
} }
} }
+55
View File
@@ -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';
}
}
+23
View File
@@ -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 = [],
) {
}
}
+243 -176
View File
@@ -4,11 +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; use Kiri;
use Kiri\Abstracts\CoordinatorManager;
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;
/** /**
* *
@@ -23,215 +29,276 @@ 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;
/** /**
* @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 * @throws
*/ */
public function scan_build_route(): void public function scan_build_route(): void
{ {
$this->read_dir_file(APP_PATH . 'routes'); $coordinator = CoordinatorManager::utility(Coordinator::WORKER_START);
$container = Kiri::getDi();
$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);
$container = Kiri::getDi(); $changedFiles = $container->get(HotReloadState::class)->consume();
$scanner = di(Kiri\Di\Scanner::class); $normalizedAppPath = str_replace('\\', '/', APP_PATH . 'app');
$scanner->load_directory(APP_PATH . 'app/Controller'); $normalizedRoutePath = str_replace('\\', '/', APP_PATH . 'routes');
$this->reset($container); $routeChanged = false;
} $appChangedFiles = [];
foreach ($changedFiles as $changedFile) {
if (str_starts_with($changedFile, $normalizedRoutePath . '/')) {
$routeChanged = true;
continue;
}
if (str_starts_with($changedFile, $normalizedAppPath . '/')) {
$appChangedFiles[] = $changedFile;
}
}
$usedArtifact = false;
if (($scanConfig['cache_enabled'] ?? false) && !$routeChanged && $artifactState->has(static::$type)) {
$artifact = $artifactState->load(static::$type);
$router = $container->get(DataGrip::class)->get(static::$type);
$usedArtifact = $router->importArtifact($artifact, $appChangedFiles);
}
if (!$usedArtifact) {
$this->read_dir_file(APP_PATH . 'routes');
}
if (!$routeChanged && !empty($appChangedFiles) && ($scanConfig['cache_enabled'] ?? false)) {
$scanner->scanFiles($appChangedFiles, APP_PATH . 'app/', null, !$usedArtifact);
} elseif (!$usedArtifact) {
$scanner->scan(APP_PATH . 'app/');
} else {
$scanner->scanFiles([], APP_PATH . 'app/', null, false);
}
$this->reset($container);
$artifactState->store(static::$type, $container->get(DataGrip::class)->get(static::$type)->exportArtifact());
$coordinator->done();
}
/** /**
* @param ContainerInterface $container * @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 * @param $path
* @return void *
* @throws * @return void
*/ * @throws
private function read_dir_file($path): void */
{ private function read_dir_file($path): void
$files = glob($path . '/*'); {
for ($i = 0; $i < count($files); $i++) { $files = glob($path . '/*');
$file = $files[$i]; for ($i = 0; $i < count($files); $i++) {
if (is_dir($file)) { $file = $files[$i];
$this->read_dir_file($file); if (is_dir($file)) {
} else { $this->read_dir_file($file);
$this->resolve_file($file); } else {
} $this->resolve_file($file);
} }
} }
}
/** /**
* @param $files * @param $files
* @throws *
*/ * @throws
private function resolve_file($files): void */
{ private function resolve_file($files): void
try { {
include "$files"; try {
} catch (\Throwable $throwable) { static::$currentSourceFile = str_replace('\\', '/', realpath($files) ?: $files);
error($throwable); include "$files";
} } catch (\Throwable $throwable) {
} \Kiri::getLogger()->json_log($throwable);
} finally {
static::$currentSourceFile = null;
}
}
public static function getCurrentSourceFile(): ?string
{
return static::$currentSourceFile;
}
} }
+501 -251
View File
@@ -6,12 +6,17 @@ namespace Kiri\Router;
use Closure; use Closure;
use Kiri\Router\Annotate\Defer;
use Kiri\Router\Annotate\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,545 @@ 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;
}
$this->methods[$requestMethod . '_' . $path] = new RouteEntry(
requestMethod: $requestMethod,
path: $path,
class: $class,
method: $method,
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'] : [],
);
}
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 string $class
* @param mixed $value * @param string $method
* @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
View File
@@ -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);
}
}
+4 -6
View File
@@ -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());
} }
+1 -1
View File
@@ -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);
} }
} }
+3 -1
View File
@@ -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
{ {
+3 -5
View File
@@ -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;
} }
+7 -6
View File
@@ -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);
} }
+48
View File
@@ -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);
}
}
+498
View File
@@ -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>