This commit is contained in:
2025-12-31 00:19:29 +08:00
parent 34051feb87
commit 91e0fe8940
12 changed files with 569 additions and 66 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ $data = [
'name' => '张三',
'email' => 'zhangsan@example.com',
'age' => 28,
'skills' => ['PHP', 'JavaScript', 'MySQL', 'Redis'],
'skills' => ['PHP', 'JavaScript', 'MySQL', 'NoSql'],
'posts' => [
[
'title' => 'Blade 模板引擎介绍',
+1 -1
View File
@@ -26,7 +26,7 @@ class ExceptionHandlerDispatcher implements ExceptionHandlerInterface
*/
public function emit(Throwable $exception, object $response): ResponseInterface
{
error($exception);
\Kiri::getLogger()->json_log($exception);
$response->withContentType(ContentType::HTML)->withBody(new Stream(throwable($exception)));
if ($exception->getCode() == 404) {
return $response->withStatus(404);
+19 -25
View File
@@ -16,30 +16,24 @@ use Psr\Http\Server\RequestHandlerInterface;
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);
try {
// 处理请求
$response = $handler->handle($request);
// 保存 Session
Session::save();
return $response;
} catch (\Throwable $e) {
// 即使出错也保存 Session
Session::save();
throw $e;
}
}
/**
* @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;
}
}
+4 -6
View File
@@ -299,9 +299,7 @@ class BladeCompiler
}
// 替换剩余的 @yield 为空
$layoutContent = preg_replace('/@yield\s*\([\'"](.+?)[\'"]\)/', '', $layoutContent);
return $layoutContent;
return preg_replace('/@yield\s*\([\'"](.+?)[\'"]\)/', '', $layoutContent);
}
// 处理 @section ... @endsection (非继承模式,用于组件等)
@@ -324,9 +322,7 @@ class BladeCompiler
}, $content);
// 处理 @parent(在 section 中使用)
$content = preg_replace('/@parent/', '', $content);
return $content;
return preg_replace('/@parent/', '', $content);
}
/**
@@ -348,6 +344,8 @@ class BladeCompiler
$data = eval("return [{$dataStr}];");
$dataCode = var_export($data, true);
} catch (\Throwable $e) {
\Kiri::getLogger()->json_log($e);
$dataCode = '[]';
}
} else {
+3
View File
@@ -61,6 +61,9 @@ class BladeView
require $compiledPath;
} catch (\Throwable $e) {
ob_end_clean();
\Kiri::getLogger()->json_log($throwable);
throw new \RuntimeException("视图渲染失败: {$this->view}", 0, $e);
}
+1
View File
@@ -87,6 +87,7 @@ class OnRequest implements OnRequestInterface
$PsrResponse = $this->router->query($request->server['path_info'], $request->getMethod())->run($PsrRequest);
} catch (Throwable $throwable) {
\Kiri::getLogger()->json_log($throwable);
$PsrResponse = $this->exception->emit($throwable, $this->constrictResponse);
} finally {
$this->responseEmitter->response($PsrResponse, $response, $PsrRequest);
+33 -26
View File
@@ -12,9 +12,9 @@ use Psr\Http\Message\StreamInterface;
/**
* 渲染 Blade 视图
*
*
* @param string $path 视图路径(支持 . 分隔,如 'user.profile'
* @param array $data 视图数据
* @param array $data 视图数据
*
* @return ResponseInterface
*/
@@ -22,26 +22,33 @@ 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');
$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);
}
// 渲染视图
$content = $factory->render($path, $data);
} catch (\Exception $e) {
$content = function_exists('throwable') ? throwable($e) : $e->getMessage();
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);
}
return $response->html($content);
}
/**
@@ -104,7 +111,7 @@ class Response implements ResponseInterface
/**
* @param array $content
* @param int $statusCode
* @param int $statusCode
*
* @return ResponseInterface
*/
@@ -115,8 +122,8 @@ class Response implements ResponseInterface
/**
* @param string $url
* @param array $params
* @param int $statusCode
* @param array $params
* @param int $statusCode
*
* @return ResponseInterface
*/
@@ -127,7 +134,7 @@ class Response implements ResponseInterface
/**
* @param array $content
* @param int $statusCode
* @param int $statusCode
*
* @return ResponseInterface
*/
@@ -139,7 +146,7 @@ class Response implements ResponseInterface
/**
* @param string $content
* @param int $statusCode
* @param int $statusCode
*
* @return ResponseInterface
*/
@@ -151,7 +158,7 @@ class Response implements ResponseInterface
/**
* @param string $content
* @param int $statusCode
* @param int $statusCode
*
* @return ResponseInterface
*/
@@ -162,8 +169,8 @@ class Response implements ResponseInterface
/**
* @param mixed $data
* @param int $statusCode
* @param mixed $data
* @param int $statusCode
* @param ContentType $type
*
* @return Response
@@ -176,7 +183,7 @@ class Response implements ResponseInterface
/**
* @param string $method
* @param mixed ...$params
* @param mixed ...$params
*
* @return mixed
*/
@@ -342,7 +349,7 @@ class Response implements ResponseInterface
* immutability of the message, and MUST return an instance that has the
* 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).
*
* @return static
@@ -376,7 +383,7 @@ class Response implements ResponseInterface
* immutability of the message, and MUST return an instance that has the
* 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).
*
* @return static
@@ -454,8 +461,8 @@ class Response implements ResponseInterface
/**
* @param string $content
* @param int $statusCode
* @param string $content
* @param int $statusCode
* @param ContentType $contentType
*
* @return ResponseInterface
@@ -479,7 +486,7 @@ class Response implements ResponseInterface
* @link http://tools.ietf.org/html/rfc7231#section-6
* @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 int $code The 3-digit integer result code to set.
* @param string $reasonPhrase The reason phrase to use with the
* provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification.
+2 -2
View File
@@ -192,7 +192,7 @@ class Router
$container = Kiri::getDi();
$scanner = $container->get(Kiri\Di\Scanner::class);
$scanner->load_directory(APP_PATH . 'app/Controller');
$scanner->scan(APP_PATH . 'app/');
$this->reset($container);
$coordinator->done();
@@ -255,7 +255,7 @@ class Router
try {
include "$files";
} catch (\Throwable $throwable) {
error($throwable);
\Kiri::getLogger()->json_log($throwable);
}
}
+1 -1
View File
@@ -133,7 +133,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
$this->register($route, $value, $handler);
}
} catch (Throwable $throwable) {
error($throwable);
\Kiri::getLogger()->json_log($throwable);
}
}
+2 -2
View File
@@ -21,8 +21,8 @@ class MixedProxy extends TypesProxy
try {
return $value == ($form->{$field} = $value);
} catch (\Throwable $throwable) {
return false;
return $this->getLogger()->json_log($throwable, [], false);
}
}
}
}
+4 -2
View File
@@ -2,7 +2,9 @@
namespace Kiri\Router\Validator\Types;
abstract class TypesProxy
use Kiri\Abstracts\Component;
abstract class TypesProxy extends Component
{
@@ -20,4 +22,4 @@ abstract class TypesProxy
*/
abstract public function dispatch(object $form, string $field, mixed $value): bool;
}
}
+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>