> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 包的 Blade 扩展与编译缓存

> 解读 Laravel 13 的 BladeCompiler，讲解自定义指令与条件语句的注册、编译时与运行时的分离，以及包更新时的视图缓存验证。

要从包中提供 Blade 语法，需要把**将模板转换为 PHP 的处理**与**执行生成的 PHP 的处理**区分开来。如果在编译时嵌入当前的配置值，缓存的视图可能会一直显示旧值。

本页参考 Laravel 官方文档 `13.x` 以及 Framework `v13.37.0` 的实现，对[包开发基础](/zh-CN/advanced/package-development)进行深入讲解。常规模板语法请参阅 [Blade](/zh-CN/blade)，视图的命名空间与发布请参阅[包视图的覆盖与更新](/zh-CN/advanced/package-views)。

## 区分注册、编译与执行

传给 `Blade::directive()` 的回调返回的不是要显示的值，而是**要生成的 PHP 代码字符串**。参数 `$expression` 也不是已求值的值，而是写在模板中的 PHP 表达式。

```mermaid theme={null}
flowchart TD
    A["在服务提供者的 boot 中注册扩展"] --> B["渲染视图"]
    B --> C{"需要编译?"}
    C -->|Yes| D["通过 directive 的回调<br>生成 PHP 代码"]
    D --> E["保存编译结果"]
    C -->|No| F["使用已保存的 PHP"]
    E --> G["执行 PHP 生成 HTML"]
    F --> G
    G --> H["引用运行时的配置与参数"]
```

| API | 注册的回调的作用 | 执行时机 |
| - | - | - |
| `Blade::directive()` | 生成 PHP 代码字符串 | 编译目标指令时 |
| `Blade::if()` | 判断条件 | 已生成的 PHP 调用 `Blade::check()` 时 |

`view:cache` 也会执行编译。在指令的生成回调中，不要获取当前用户、写入数据库或调用外部 API。

## 使用包专属的名称注册

作为示例，我们提供一个在配送编号前显示所配置标签的 `@acmeCourierLabel($code)`。前提是通过 Composer 的 PSR-4 将 `Acme\Courier\` 映射到 `src/`，并通过[自动发现](/zh-CN/advanced/package-discovery)或手动方式注册服务提供者。

```php src/CourierServiceProvider.php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Blade::directive('acmeCourierLabel', function (string $expression): string {
            return "<?php echo e((string) config('courier.label', 'Tracking').': '.({$expression})); ?>";
        });

        Blade::if('acmeCourierEnabled', function (): bool {
            return (bool) config('courier.enabled', false);
        });
    }
}
```

本示例约定 `courier.label` 为字符串、`courier.enabled` 为布尔值。配置文件的分发与合并请参阅[包配置的合并与缓存](/zh-CN/advanced/package-config-merging)。

注册在常规服务提供者的 `boot()` 中进行，不要用 `runningInConsole()` 条件包裹。HTTP 渲染和 Artisan 的 `view:cache` 都需要相同的扩展。也要避免把只注册 Blade 扩展的服务提供者设为延迟提供者，因为它在其他服务被解析之前不会启动。

### 参数以表达式而非值的形式接收

```blade theme={null}
@acmeCourierLabel($code)
```

在这个模板中，回调接收到的是字符串 `$code`。生成的 PHP 如下所示。

```php theme={null}
<?php echo e((string) config('courier.label', 'Tracking').': '.($code)); ?>
```

`config()` 与 `$code` 的求值发生在执行这段 PHP 的时刻。这与回调自身读取 `config()` 并返回固定字符串的实现不同。

经确认，`callCustomDirective()` 会去掉外层圆括号并 trim 表达式，然后传给回调。它不会把参数转换为 PHP 值，也不会拆分为单独的参数。本示例将其作为**传入一个必需表达式的语法**公开。

处理多个参数时，不要简单地用逗号 `explode()`。数组、方法调用和字符串字面量中也可能包含逗号。如果要处理属性或复杂的 HTML，作为 [Blade 组件](/zh-CN/blade)提供更容易划分职责。

<Warning>
  模板中的表达式是由可信开发者编写的 PHP。不要用用户输入拼接模板源码。另外，自定义指令中的 `echo` 不会自动获得常规 `{{ }}` 的转义。本示例中，生成的 PHP 显式调用了 `e()`。
</Warning>

### 名称冲突也是兼容性问题

`directive()` 以名称为键将处理器保存到数组中。同名注册会覆盖之前的注册，并且 `compileStatement()` 会优先选择自定义指令，而不是内置的转换方法。官方测试中也有通过自定义注册覆盖 `foreach` 的用例。

请避免使用 `label`、`enabled` 等通用名称以及内置名称，并加上包专属的前缀。将使用者在模板中使用的名称和参数视为公开 API，变更时提供迁移步骤。

## 条件分支使用 Blade::if 在运行时判断

上面服务提供者中注册的条件可以这样使用。

```blade theme={null}
@acmeCourierEnabled
    @acmeCourierLabel($code)
@else
    配送追踪已禁用。
@endacmeCourierEnabled
```

`Blade::if()` 在保存条件回调之后，会注册用于开始、`unless`、`else` 和结束的指令。本示例的开始部分会被转换为以下 PHP。

```php theme={null}
<?php if (\Illuminate\Support\Facades\Blade::check('acmeCourierEnabled')): ?>
```

检查配置的条件回调由 `check()` 在运行时调用。这并不是在编译时固定 true 或 false 结果的处理。即使是使用已编译视图的 HTTP 进程，也需要注册条件回调。

这种语法只用于切换显示内容，并不是对配送操作的授权。访问控制也要在路由或操作一侧实施。

## 包更新后视图未必会自动重新生成

经确认，`Compiler::isExpired()` 会检查缓存是否存在，并根据配置检查原始视图与已编译文件的更新时间。**它不会把服务提供者中编写的指令变更作为依赖文件进行追踪。**

| 变更内容 | 对已编译 PHP 的影响 |
| - | - |
| 修改 `directive()` 返回的代码 | 旧的 PHP 仍会保留，需要重新编译 |
| 仅修改 `Blade::if()` 条件回调的处理逻辑 | 已生成的 `check()` 可以调用新的逻辑，但需要加载新代码 |
| 修改指令名称或参数约定 | 需要修改使用者的模板并重新编译 |
| 视图已发布到应用一侧 | 仅更新包内的视图不会更新已发布的副本 |

官方文档也建议在修改指令逻辑后，使用 `view:clear` 删除缓存。请在使用方应用的部署流程中，在更新的包与使用者的视图保持一致之后执行。

```bash theme={null}
php artisan view:clear
php artisan view:cache
```

`view:cache` 在生产环境中进行预编译时执行。不要在包的 `boot()` 中每次都清除缓存，也不要在安装时擅自改写使用方应用的部署流程。配置缓存的更新和长驻进程的重新加载，也请遵循使用环境的常规部署步骤。

## 分别验证编译结果与渲染结果

首先，在注册了上述服务提供者的测试用 Laravel 应用中，验证生成的 PHP。以下是添加到继承 `Tests\TestCase` 的 Feature 测试中的方法。

```php theme={null}
public function test_label_directive_compiles_to_runtime_lookup(): void
{
    $compiled = \Illuminate\Support\Facades\Blade::compileString(
        '@acmeCourierLabel($code)',
    );

    $this->assertSame(
        "<?php echo e((string) config('courier.label', 'Tracking').': '.(\$code)); ?>",
        $compiled,
    );
}
```

这个测试可以确认生成 PHP 的约定，但无法验证运行时的转义和条件分支。在实际的包中，还要添加视图渲染和更新时的验证。

| 验证用例 | 预期结果 |
| - | - |
| 向配送编号传入会被解析为 HTML 的字符 | 通过生成的 `e()` 进行转义 |
| 使用同一个已编译视图，在渲染前更改标签配置 | 下一次渲染显示新的配置值 |
| 将 `courier.enabled` 设为 true 或 false 后渲染 | 只显示对应的分支 |
| 执行 `view:cache` 后启动新的应用 | 扩展已注册，结果与未预编译时相同 |
| 修改指令的生成代码后部署 | 重新生成缓存后使用新的 PHP |
| 更新支持的 Laravel 版本 | 生成代码、渲染和缓存使用的各项测试均通过 |

## 相关页面

<Columns cols={2}>
  <Card title="包视图的覆盖与更新" icon="eye" href="/zh-CN/advanced/package-views">
    了解命名空间的查找顺序以及维护已发布模板的方法。
  </Card>

  <Card title="包的缓存与 optimize 集成" icon="gears" href="/zh-CN/advanced/package-optimization">
    了解将自有缓存的构建与删除集成到部署流程中的方法。
  </Card>
</Columns>

## 参考的一手资料

* [Laravel 官方文档：Extending Blade・Custom if statements](https://github.com/laravel/docs/blob/12080c72699b99ec6573f7e79ff4834d9001b6bc/blade.md#extending-blade)
* [Laravel 官方文档：包的视图与组件](https://github.com/laravel/docs/blob/12080c72699b99ec6573f7e79ff4834d9001b6bc/packages.md#views)
* [BladeCompiler：directive()・callCustomDirective()・compileStatement()・if()・check()](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/View/Compilers/BladeCompiler.php)
* [Compiler：基于 isExpired() 的缓存判断](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/View/Compilers/Compiler.php)
* [BladeCustomTest：自定义指令、条件以及覆盖内置语法](https://github.com/laravel/framework/blob/v13.37.0/tests/View/Blade/BladeCustomTest.php)


## Related topics

- [进阶主题](/zh-CN/advanced/index.md)
- [Blade 模板](/zh-CN/blade.md)
- [包视图的覆盖与更新](/zh-CN/advanced/package-views.md)
- [缓存](/zh-CN/cache.md)
- [包的路由注册与缓存](/zh-CN/advanced/package-routes.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.