eee
This commit is contained in:
@@ -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 模板引擎介绍',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -27,7 +27,6 @@ class SessionMiddleware implements MiddlewareInterface
|
||||
// 启动 Session
|
||||
Session::start($request);
|
||||
|
||||
try {
|
||||
// 处理请求
|
||||
$response = $handler->handle($request);
|
||||
|
||||
@@ -35,11 +34,6 @@ class SessionMiddleware implements MiddlewareInterface
|
||||
Session::save();
|
||||
|
||||
return $response;
|
||||
} catch (\Throwable $e) {
|
||||
// 即使出错也保存 Session
|
||||
Session::save();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+12
-5
@@ -36,12 +36,19 @@ function View(string $path, array $data = []): ResponseInterface
|
||||
}
|
||||
|
||||
// 渲染视图
|
||||
$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);
|
||||
|
||||
return $response->html($content);
|
||||
ob_start();
|
||||
|
||||
extract(['errorData' => $throwable], EXTR_SKIP);
|
||||
include __DIR__.'/template/error.php';
|
||||
|
||||
$message = ob_get_clean();
|
||||
|
||||
return $response->html($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
|
||||
$this->register($route, $value, $handler);
|
||||
}
|
||||
} catch (Throwable $throwable) {
|
||||
error($throwable);
|
||||
\Kiri::getLogger()->json_log($throwable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class MixedProxy extends TypesProxy
|
||||
try {
|
||||
return $value == ($form->{$field} = $value);
|
||||
} catch (\Throwable $throwable) {
|
||||
return false;
|
||||
return $this->getLogger()->json_log($throwable, [], false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace Kiri\Router\Validator\Types;
|
||||
|
||||
abstract class TypesProxy
|
||||
use Kiri\Abstracts\Component;
|
||||
|
||||
abstract class TypesProxy extends Component
|
||||
{
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<title>Error: <?= htmlspecialchars($errorData['type']) ?></title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.error-title i {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.error-subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.error-section {
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: #f5f5f5;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-toggle {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #282c34;
|
||||
color: #abb2bf;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
color: #61afef;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
color: #98c379;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e06c75;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f5f5f5;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.trace-item {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #667eea;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.trace-header {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.trace-location {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.environment-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.error-container {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.environment-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-header">
|
||||
<h1 class="error-title">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<?= htmlspecialchars($errorData['type']) ?>
|
||||
</h1>
|
||||
<div class="error-subtitle">
|
||||
<?= htmlspecialchars($errorData['message']) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-content">
|
||||
<!-- Error Details -->
|
||||
<div class="error-section">
|
||||
<div class="section-header" onclick="toggleSection('details')">
|
||||
<span class="section-title">Error Details</span>
|
||||
<span class="section-toggle" id="toggle-details">▼</span>
|
||||
</div>
|
||||
<div class="section-content" id="section-details">
|
||||
<div class="code-block">
|
||||
<span class="file-path"><?= htmlspecialchars($errorData['file']) ?></span>
|
||||
<span class="line-number">:<?= $errorData['line'] ?></span>
|
||||
<br><br>
|
||||
<span class="error-message"><?= htmlspecialchars($errorData['message']) ?></span>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td><span class="badge badge-error"><?= htmlspecialchars($errorData['type']) ?></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<td><?= $errorData['code'] ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<td><?= $errorData['timestamp'] ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stack Trace -->
|
||||
<div class="error-section">
|
||||
<div class="section-header" onclick="toggleSection('trace')">
|
||||
<span class="section-title">Stack Trace</span>
|
||||
<span class="section-toggle" id="toggle-trace">▼</span>
|
||||
</div>
|
||||
<div class="section-content" id="section-trace">
|
||||
<?php
|
||||
foreach ($errorData['trace'] as $index => $trace): ?>
|
||||
<div class="trace-item">
|
||||
<div class="trace-header">
|
||||
#<?= $index ?>:
|
||||
<?= $trace['class'] ?? '' ?><?= $trace['type'] ?? '' ?><?= $trace['function'] ?>()
|
||||
</div>
|
||||
<div class="trace-location">
|
||||
<?= $trace['file'] ?? 'internal' ?><?= isset($trace['line']) ? ':' . $trace['line'] : '' ?>
|
||||
</div>
|
||||
<?php
|
||||
if (!empty($trace['args'])): ?>
|
||||
<div style="margin-top: 5px; font-size: 12px; color: #666;">
|
||||
Arguments: <?= implode(', ', $trace['args']) ?>
|
||||
</div>
|
||||
<?php
|
||||
endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Information -->
|
||||
<div class="error-section">
|
||||
<div class="section-header" onclick="toggleSection('server')">
|
||||
<span class="section-title">Server Information</span>
|
||||
<span class="section-toggle" id="toggle-server">▼</span>
|
||||
</div>
|
||||
<div class="section-content hidden" id="section-server">
|
||||
<table>
|
||||
<?php
|
||||
foreach ($errorData['server'] as $key => $value): ?>
|
||||
<tr>
|
||||
<th><?= htmlspecialchars($key) ?></th>
|
||||
<td><?= htmlspecialchars($value) ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request Information -->
|
||||
<div class="error-section">
|
||||
<div class="section-header" onclick="toggleSection('request')">
|
||||
<span class="section-title">Request Information</span>
|
||||
<span class="section-toggle" id="toggle-request">▼</span>
|
||||
</div>
|
||||
<div class="section-content hidden" id="section-request">
|
||||
<h4>GET Parameters</h4>
|
||||
<?php
|
||||
if (!empty($errorData['request']['GET'])): ?>
|
||||
<table>
|
||||
<?php
|
||||
foreach ($errorData['request']['GET'] as $key => $value): ?>
|
||||
<tr>
|
||||
<th><?= htmlspecialchars($key) ?></th>
|
||||
<td><?= htmlspecialchars(is_array($value) ? json_encode($value) : $value) ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
</table>
|
||||
<?php
|
||||
else: ?>
|
||||
<p>No GET parameters</p>
|
||||
<?php
|
||||
endif; ?>
|
||||
|
||||
<h4>POST Parameters</h4>
|
||||
<?php
|
||||
if (!empty($errorData['request']['POST'])): ?>
|
||||
<table>
|
||||
<?php
|
||||
foreach ($errorData['request']['POST'] as $key => $value): ?>
|
||||
<tr>
|
||||
<th><?= htmlspecialchars($key) ?></th>
|
||||
<td><?= htmlspecialchars(is_array($value) ? json_encode($value) : $value) ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
</table>
|
||||
<?php
|
||||
else: ?>
|
||||
<p>No POST parameters</p>
|
||||
<?php
|
||||
endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment Info -->
|
||||
<div class="environment-info">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Memory Usage</div>
|
||||
<div class="info-value"><?= $errorData['memory_usage'] ?></div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Peak Memory</div>
|
||||
<div class="info-value"><?= $errorData['peak_memory'] ?></div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Execution Time</div>
|
||||
<div class="info-value"><?= round($errorData['execution_time'], 4) ?> seconds</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">PHP Version</div>
|
||||
<div class="info-value"><?= PHP_VERSION ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="window.location.reload()">
|
||||
<i class="fas fa-redo"></i> Reload Page
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="window.history.back()">
|
||||
<i class="fas fa-arrow-left"></i> Go Back
|
||||
</button>
|
||||
<a href="/" class="btn btn-secondary">
|
||||
<i class="fas fa-home"></i> Go Home
|
||||
</a>
|
||||
<button class="btn btn-secondary" onclick="copyErrorDetails()">
|
||||
<i class="fas fa-copy"></i> Copy Error Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSection(sectionId) {
|
||||
const section = document.getElementById(`section-${sectionId}`);
|
||||
const toggle = document.getElementById(`toggle-${sectionId}`);
|
||||
|
||||
if (section.classList.contains('hidden')) {
|
||||
section.classList.remove('hidden');
|
||||
toggle.textContent = '▲';
|
||||
} else {
|
||||
section.classList.add('hidden');
|
||||
toggle.textContent = '▼';
|
||||
}
|
||||
}
|
||||
|
||||
function copyErrorDetails() {
|
||||
const errorDetails = {
|
||||
type: '<?= addslashes($errorData['type']) ?>',
|
||||
message: '<?= addslashes($errorData['message']) ?>',
|
||||
file: '<?= addslashes($errorData['file']) ?>',
|
||||
line: <?= $errorData['line'] ?>,
|
||||
timestamp: '<?= $errorData['timestamp'] ?>',
|
||||
trace: <?= json_encode($errorData['trace']) ?>
|
||||
};
|
||||
|
||||
const text = `Error Details:
|
||||
Type: ${errorDetails.type}
|
||||
Message: ${errorDetails.message}
|
||||
File: ${errorDetails.file}
|
||||
Line: ${errorDetails.line}
|
||||
Timestamp: ${errorDetails.timestamp}
|
||||
|
||||
Stack Trace:
|
||||
${JSON.stringify(errorDetails.trace, null, 2)}`;
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('Error details copied to clipboard!');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
alert('Failed to copy error details');
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-expand first section
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
toggleSection('details');
|
||||
toggleSection('trace');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user