This commit is contained in:
2025-12-01 07:16:58 +08:00
parent 89ac36c227
commit 153667f0b4
4 changed files with 715 additions and 6 deletions
+267 -2
View File
@@ -113,20 +113,32 @@ class BladeCompiler
// 移除注释 // 移除注释
$content = $this->compileComments($content); $content = $this->compileComments($content);
// 编译 Echo 语句 // 编译 Echo 语句(包括 @json
$content = $this->compileEchos($content); $content = $this->compileEchos($content);
// 编译指令 // 编译指令
$content = $this->compileDirectives($content); $content = $this->compileDirectives($content);
// 编译条件判断语法糖
$content = $this->compileConditionalDirectives($content);
// 编译布局和继承(如果未跳过) // 编译布局和继承(如果未跳过)
if (!$skipLayouts) { if (!$skipLayouts) {
$content = $this->compileLayouts($content); $content = $this->compileLayouts($content);
} }
// 编译包含 // 编译包含和组件
$content = $this->compileIncludes($content); $content = $this->compileIncludes($content);
// 编译 @each 指令
$content = $this->compileEach($content);
// 编译栈和推送
$content = $this->compileStacks($content);
// 编译表单辅助
$content = $this->compileFormHelpers($content);
// 编译原始 PHP // 编译原始 PHP
$content = $this->compilePhp($content); $content = $this->compilePhp($content);
@@ -148,6 +160,7 @@ class BladeCompiler
* 编译 Echo 语句 * 编译 Echo 语句
* {{ $var }} => <?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8'); ?> * {{ $var }} => <?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8'); ?>
* {!! $var !!} => <?php echo $var; ?> * {!! $var !!} => <?php echo $var; ?>
* @json($var) => JSON 编码输出
* *
* @param string $content * @param string $content
* @return string * @return string
@@ -166,6 +179,12 @@ class BladeCompiler
return "<?php echo {$expression}; ?>"; return "<?php echo {$expression}; ?>";
}, $content); }, $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; return $content;
} }
@@ -214,6 +233,25 @@ class BladeCompiler
$content = preg_replace('/@break\b/', '<?php break; ?>', $content); $content = preg_replace('/@break\b/', '<?php break; ?>', $content);
$content = preg_replace('/@continue\b/', '<?php continue; ?>', $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; return $content;
} }
@@ -321,6 +359,233 @@ class BladeCompiler
}, $content); }, $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 * 编译原始 PHP @php ... @endphp
* *
+4
View File
@@ -49,6 +49,10 @@ class BladeView
// 提取数据 // 提取数据
extract($this->data, EXTR_SKIP); extract($this->data, EXTR_SKIP);
// 初始化栈和 section 数组
$__stacks = [];
$__sections = [];
// 开始输出缓冲 // 开始输出缓冲
ob_start(); ob_start();
+39 -4
View File
@@ -4,15 +4,50 @@
## 功能特性 ## 功能特性
### 基础功能
- ✅ 变量输出:`{{ $variable }}``{!! $variable !!}` - ✅ 变量输出:`{{ $variable }}``{!! $variable !!}`
-控制结构:`@if`, `@elseif`, `@else`, `@endif` -JSON 输出:`@json($data)`
-循环:`@foreach`, `@endforeach`, `@for`, `@endfor`, `@while`, `@endwhile` -控制结构:`@if`, `@elseif`, `@else`, `@endif`, `@unless`
-布局继承:`@extends`, `@section`, `@yield`, `@endsection` -循环:`@foreach`, `@for`, `@while`, `@forelse` 及其结束指令
-包含视图:`@include` -Switch`@switch`, `@case`, `@default`, `@endswitch`
- ✅ 布局继承:`@extends`, `@section`, `@yield`, `@endsection`, `@parent`
- ✅ 包含视图:`@include`, `@each`
- ✅ 注释:`{{-- comment --}}` - ✅ 注释:`{{-- comment --}}`
- ✅ 原始 PHP`@php ... @endphp` - ✅ 原始 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` - 条件样式
## 使用方法 ## 使用方法
### 基本用法 ### 基本用法
+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>
```