Skip to content

产物与部署 ​

前置:编译渲染、组件;本页:pure compile 产物、pure check 契约检查、无依赖视图与部署缓存。

磁盘缓存仍会在每个请求中重建 Shape 树。若要在部署时完全不构建 Shape,可以用 pure 命令提前编译。

预编译产物 ​

bash
vendor/bin/pure compile src

每个 *.cmp.php 单元会被编译成相邻的 *.pure.php 产物。产物带有 Shape 指纹并返回一个 Renderer,因此既不需要 Shape 树,也不需要编译缓存:

php
<?php

$page = require __DIR__ . '/page.pure.php';

echo $page->render(['title' => 'Users']);
$page->save(__DIR__ . '/out.html', ['title' => 'Users']);

进阶:独立的 *.shape.php 模板

除单元外,pure compile 也会发现返回 Shape 的 *.shape.php 文件——没有调用函数的模板。 组件才是推荐形态;只有当没有组件可以承载该模板时才使用 shape 文件。

  • pure compile <路径>... 接受文件与目录(递归查找),同时发现 *.cmp.php 单元与 *.shape.php 模板,并跳过内容已最新的文件:shape 仍会被加载与编译(因此它引用的任何文件 发生变化都能被感知),但内容一致的文件会以 unchanged: 报告而不是重写。 --list 不编译,直接打印实际类型:name -> file (component)、 file (shape),以及带 #[Template] 的 name -> file (template)。pure compile --check 不写入任何 文件,当产物过期或缺失时以退出码 1 结束,适合放在 CI 步骤中。--plain 会额外写出下文的 「无依赖视图」,--check --plain 同时校验两种形态。典型输出是 Card -> components/Card.cmp.php (component);独立模板会显示为 views/page.shape.php (shape),带 #[Template] 的构建器会显示为 pageShape -> views/page.cmp.php (template)。
  • 产物的渲染结果与运行时编译器完全一致,并且读起来就像模板: 标记仍是标记,动态值写成 <?= ... ?>,控制流使用替代语法,闭包只定义一次并导入类的短名。 HTML 片段承载的是精确的渲染字节,因此不会被重新缩进。产物的 Renderer::$source 为空——文件本身就是源码。
  • 动态值通过 TemplateRuntime 读取 Slot,编译语义集中在一处:必填 Slot 缺失时抛出 MissingSlotException(必填的值 Slot 与 raw Slot 显式传入 null 也会失败,属性 Slot 则保持 按 null 省略),default: 提供可选 Slot 的编译期默认值,转义与强制转换与平铺渲染器完全一致; 仅当 Slot 路径与键名不同时才出现 path:。
  • 产物还携带根作用域 Slot 清单(Renderer::$slots),因此开发守卫无需重建 Shape 树就能报告 模板从未读取的 binding。

下面是生成文件的示意源码,不是应用 API。TemplateRuntime 的 import 由 pure compile 生成;应用代码仍应调用 Renderer::render():

php
<?php

$pureBody = static function (array $v): string {
    ob_start();
    try { ?><div class="card"><h1><?= TemplateRuntime::text($v, 'title') ?></h1><ul><?php
        foreach (TemplateRuntime::items($v, 'items') as $item1):
            $v2 = TemplateRuntime::scope($item1, 'items[]'); ?><li><?= TemplateRuntime::text($v2, 'label', path: 'items[].label') ?></li><?php
        endforeach; ?></ul></div><?php
    } finally {
        $out = (string)ob_get_clean();
    }

    return $out;
};

没有自动的文档声明:树按原样渲染,整份文档的文档声明(HTML 根是 <!DOCTYPE html>,XML/SVG 根是 XML 声明)由调用方拼接——标签树或组件调用交给 renderHTML() / renderXML(),直接渲染 Renderer 时用根标签的 documentHeader()。 examples/bootstrap 的组件都是建立在其上的单元:

php
<?php

use Pure\Component\Call;
use Pure\Core\Slot;

use function Pure\Component\{component, register};
use function Pure\SVG\{svg, svgUse};

function Icon(): Call
{
    return component(__FUNCTION__);
}

register(Icon(...),
    factory: static fn () =>
        svg(
            svgUse()->href(Slot::value('href'))
        )->class(Slot::value('class')),
    prepare: static function (string $href, string $class = 'bi'): array {
        return ['href' => $href, 'class' => $class];
    }
);
php
<?php

use Pure\Component\Binds;
use Pure\Component\Call;
use Pure\Core\Slot;

use function Pure\Component\{component, register};
use function Pure\HTML\{body, head, html, title};
use function Pure\Utils\renderHTML;

function Features(): Call
{
    return component(__FUNCTION__);
}

register(Features(...),
    factory: static fn () =>
        html(
            head(title(Slot::value('title'))),
            body(Slot::raw('content'))
        ),
    prepare: #[Binds('title', 'content')] static fn (): array => [
        // 页面决定有哪些区块;每个区块取自己的记录。
        'title' => FeaturesService::pageTitle(),
        'content' => FeaturesBody(),
    ]
);

function featuresPage(): string
{
    // renderHTML() 补上文档声明;树本身不带文档声明。
    return renderHTML(component('Features'));
}

每个区块都从 service 层取自己的记录(如上面的 FeaturesService::pageTitle() 和 FeaturesBody()),因此页面函数不携带页面数据,给组件加一个 prop 也永远不需要改页面。

子组件渲染出的字符串直接进入 raw Slot——无需 (string) 强制转换——它们组成的列表按顺序拼接。

公共 component() 辅助函数会在单元或 shape 文件旁边存在产物、且产物不早于源文件时直接 加载产物,否则调用注册的工厂(每个编译 generation 一次)或编译 shape 文件(磁盘缓存仍然 生效)。它只产出片段——要文档声明就由调用方自己拼接。底层的 Registry::component() binder 属于内部细节,应用代码不应依赖其签名。

bootstrap 示例的 PlainFeaturesController 把同一份 bindings 交给示例自带的 plain() 助手(一个应用 函数:它 require 视图文件并展开数据);单一入口 public/index.php 为每个页面同时提供两种形态:/pure/features、/pure/pricing 走页面函数, /plain/features、/plain/pricing 走普通视图,开发时可以对照。

  • 请使用与生产环境相同的 PHP 次版本号构建产物:Shape::id() 的盐值包含 PHP_MAJOR_VERSION.PHP_MINOR_VERSION,产物头部也记录 PHP 版本;不要跨 PHP 次版本复制 产物或缓存。
  • 产物是构建输出:修改 Shape 后需要重新构建。加载时不会校验 Shape 树,因此请用 --check 发现过期产物。
  • 产物还带有它的 Compile::CACHE_VERSION:加载由库的其他版本写出的产物时,抛出的是带 pure compile 提示的异常,而不是在产物所描述的 Renderer 签名上报错。版本号提升本身就会 使 Compile::cachePath() 里的渲染器失效,却绝不会使模板旁的产物失效,所以 pure compile 是升级流程的一部分。*.plain.php 视图只在注释中携带版本号、没有可执行守卫,升级后会 静默输出过期内容,直到 pure compile --check --plain 发现不一致。
  • 新鲜度有两套规则,并不是同一个检查。请求时只要产物的 filemtime() 不比源单元旧就直接 使用:整秒粒度意味着与单元在同一秒写入的产物已经可用,tar、rsync 或 git checkout 造成的 touch 式时间偏移也不会丢弃它并逐请求重新编译。pure compile --check 则是更严格的构建期 闸门——它重新生成期望的源码,再与产物逐字节比较。
  • 两个会写出同一个产物的源文件(a.shape.php 紧邻 a.cmp.php)会被 pure compile 一并 拒绝并返回退出码 1,因此 a.pure.php 归属哪个模板不会由发现顺序决定。
  • 加载 Shape 文件时产生的输出会被丢弃;pure compile 只输出构建信息。

契约检查 ​

pure check 校验每个单元的契约,让不匹配在 CI 中失败,而不是等到渲染时。它不是沙箱: 为了检查单元,它会 require 该文件,并在存在时执行注册的 factory 来构建 Shape。只应在隔离的 CI 进程中对可信源码运行;它不会写入产物。

bash
vendor/bin/pure check src
  • 模板读取的 Slot 与单元返回的 bindings:模板不读取的 binding 是错误(并给出 did you mean 建议),必填 Slot 没有被绑定也是错误。prepare() 钩子把 props 变成绑定: 返回字面量数组时,这些键会被解析;返回的是 ...bindings() 助手结果或运行时计算值时, 用 #[Binds(...)] 声明键名,否则将报告为 info。
  • prepare() 钩子的参数类型与 Slot 种类:列表 Slot 需要可迭代值、child 作用域需要数组、 值 Slot 需要可字符串化值、raw Slot 两者皆可。必填 Slot 对应的可空参数是警告(绑定 null 会抛出 MissingSlotException);既未被函数使用、也不是模板 Slot 的参数同样是警告。
  • 没有 prepare() 的单元改为检查调用点:props 就是模板 Slot,setter 必须匹配;旁边的 函数若不返回 Pure\Component\Call 会报错。
  • 声明注解用来表达签名说不出的信息。#[Prop] 带 slot(该 prop 绑定的 Slot 名)、 item(列表 prop 的每一项在 Slot::each 的 item Shape 里填的 Slot)、required(调用方 义务)与 deprecated(迁移提示);#[Trusted] 标记携带已渲染标记的 prop,它必须绑定 raw Slot,且调用方传入的不是 Pure\Core\Markup 时开发守卫会告警;#[Binds] 用在 hook 或 ...bindings() 辅助函数上,在字面量数组读不出来时声明返回的键。声明会与签名和 模板比对;当 prepare() 返回的不是一个可读的字面量数组时,模板的必填 Slot 改为与声明 比对;调用点绑定了已废弃的 prop 会得到警告。
  • 每个被检查文件里的链式调用:目标不接受的 ->prop(...) 是错误(附 did you mean 建议),->children(...) 会提示应把 children 传给调用本身,从变量展开的 prop 集合会 被跳过。绑定为数组字面量的列表 prop 会逐项与 Slot 的 item Shape 比对(缺少必填键、读取了 Shape 不认识的键都是错误)。目标必须在被检查的文件中,才能得知它接受哪些 props。
  • 同一个模板把一个 Slot 同时用作标量(value/raw)与作用域(child/each)是错误; *.shape.php 模板也会做这项检查。

有错误时退出码为 1,带 --strict 时警告也返回 1。pure check 不看产物—— 产物新鲜度由 pure compile --check 负责。

如何阅读输出 ​

每条诊断都是一行 path:line: level: message,正是编辑器和 CI 注解解析器能识别的 形式。行号指向真正需要改动的那一行:链式调用指向 ->prop(...) 这个 setter, 单元级诊断只指向文件、不带行号。

text
components/Page.cmp.php:21: error: component 'Card': the call binds 'titel', which the target does not accept (did you mean 'title'?)

checked N unit(s): E error(s), W warning(s) 汇总与本次运行的结果对账;本次运行 打印了 info 发现时,会追加 , X note(s)——这些提示不会让命令失败,但它们说明了 哪些内容没能被检查。

结果与问题走不同的流。ok: 行、info 提示和 checked N unit(s) 汇总属于结果, 留在 stdout;错误、警告,以及 pure compile --check 的 missing: / stale: 属于问题,写入 stderr。于是 pure check . 2>/dev/null 留下的是结果——并不完全等于 "只留通过的部分",因为汇总行仍会报出计数;pure check . 1>/dev/null 留下的是问题, 其中包含非 --strict 下不会让命令失败的警告。请以退出码作为门禁,用汇总行读计数。

三个退出码含义互不相同,脚本可以据此区分:

退出码含义
0没有任何问题,或 --check 下没有过期产物
1有错误、--strict 下有警告、产物过期,或某个文件无法读取
2命令行本身写错了:未知命令、未知选项,或缺少路径

组件产物与缓存策略 ​

每个组件都是一个 *.cmp.php 单元,因此 pure compile 会像其他模板一样为它构建产物。绑定器会比较产物与单元文件的 mtime:产物较新就直接加载(不调用 工厂、不建树、不算指纹),过期或缺失则调用注册的工厂或编译 shape 文件。CI 中用 pure compile --check 可以发现过期产物(退出码 1)。

Compile::cachePath() 会以 0700 创建缺失目录,并拒绝组或其它用户可写的权限;当 POSIX 属主 API 可用时也会拒绝属主不符的目录,但不会检查路径是否位于 Web 根目录之外。请把缓存放在 文档根目录之外的专用目录,并使用严格的权限。

开启哪些取决于部署形态:

  • PHP-FPM——开启 Compile::cachePath() 并构建产物。没有产物时,每个请求都要为该组件 重建 Shape 树、遍历指纹,然后才渲染。产物会把这部分工作替换为一次 require。
  • 长驻 worker(RoadRunner、Swoole、FrankenPHP)——开启 Compile::cachePath() 并保留 内部绑定器的路径缓存(内联树用 static $render);renderer 常驻内存,产物可选。
  • opcache.preload——preload 只把代码常驻内存,不会让 static 变量跨请求保留(PHP preload RFC 已明确说明),因此不能替代上面两种做法。

开启 opcache 后,生产环境可以直接加载已编译的组件代码;选择缓存布局前, 请在实际部署中自行实测。

无依赖导出(可选) ​

pure compile --plain 会在产物旁边额外写出 *.plain.php:只有标记与原生 PHP, 渲染时不需要安装 purephp。加载时把数据展开成局部变量,并把展开操作放进隔离闭包,避免覆盖调用方的局部变量:

php
<?php

$render = static function (array $viewData): string {
    extract($viewData, EXTR_SKIP);
    ob_start();

    try {
        require 'views/index.plain.php';
        $output = (string) ob_get_clean();
    } catch (\Throwable $error) {
        ob_end_clean();
        throw $error;
    }

    return $output;
};

$html = $render($data);

EXTR_SKIP 不会覆盖已经存在的变量;如果数据键与 viewData 等局部变量同名,该键会被 跳过。在旧式的顶层加载写法中,data、html 等键可能与加载器自身冲突。请检查生成的 视图并显式映射这些键(只有在意图明确的可信隔离作用域中才使用 EXTR_OVERWRITE)。

需要让视图脱离库运行时才用 --plain:例如部署只带 public/ 与 views/, 或把模板目录交给其他人。常规数据下无依赖视图逐字节等于产物,并且仅当其根是文档根 (<html> 或 XML 树)时前面补上文档声明:页面保留自己的 <!DOCTYPE html> / XML 声明, 而片段(div、内联 SVG 图标)视图直接以标记开头,因此被 include 时绝不会把文档声明 插进文档中间。而且它是最快的形态:值直接进入 htmlspecialchars(),没有运行时访问器调用。 但它是普通视图而非编译组件,严格 Slot 语义仍由产物提供——语义差异与 @var 注解说明见无依赖视图注意事项。

无依赖视图注意事项 ​

无依赖视图是标记 + 原生 PHP——脱离 purephp 也能渲染,但它不携带编译渲染器的 严格 Slot 语义:

  • 必填 Slot 缺失是未定义变量,不再抛出 MissingSlotException;
  • 必填 Slot 显式传入 null 时渲染为空,而不是像产物那样(值 Slot 与 raw Slot)失败;
  • 文档根的视图会带上 <!DOCTYPE html> / XML 声明,而片段视图以标记开头;
  • null 属性输出为空值,而不是整个属性消失;
  • 列表 Slot 不再校验可迭代性,值的字符串化交给 PHP 而不是 SlotRuntime;
  • raw Slot 与产物一样用 implode('') 拼接可字符串化值的可迭代集合;PHP 无法字符串化的元素 会被强转(警告 + Array),而不是像产物那样抛 InvalidArgumentException。

顶层 Slot 读取为普通变量,嵌套 Slot 读取为它所在的数组,转义直接内联,因此它和手写 模板一样可移植。

使用组件时,控制器会先渲染组件、再把它们的标记作为 raw bindings 传给页面 Shape,因此视图文件仍然无依赖,而请求处理器会用到库。

视图会按 Shape 结构为每个顶层 Slot 生成 @var 注解,静态分析器无需排除规则或额外 配置即可读取这些展开的局部变量:

php
<?php

/**
 * @var scalar|null|\Stringable $title
 * @var array{columns: ...} $content
 */

值 Slot 是 scalar|null|\Stringable(即 htmlspecialchars() 可接受的类型),条件 Slot 是 mixed,child 与列表作用域会推导成 array shape 及它们的 iterable;特殊 Slot 名声明在加载器的 $data 数组上。这些注解只是注释,不增加任何输出字节。唯一仍会告警的 是没有数组默认值的可选容器 Slot:生成的读取会回退到 null,注解如实反映这一点。

视图是 include,请在生产开启 opcache:关闭时每次渲染都会重新解析文件。

可信产物与 CSP ​

*.pure.php 是由源码树生成的可执行 PHP。源码与生成产物都应视为可信代码:在受控 CI 中 构建并验证产物,限制写入权限,不要把产物作为公开静态文件提供,也不要接受不可信用户上传的 产物。无依赖视图虽然运行时不依赖库,但同样属于生成代码。

Raw::of()、Slot::raw() 与组件 Markup 子节点都会绕过转义,不会替你校验 URL、事件处理器、 样式或脚本。请在应用边界校验这些输入;页面有意包含活动标记时,还应发送合适的浏览器 Content Security Policy,例如:

http
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'

请按应用可信资产调整策略;CSP 是纵深防御,不能替代输入校验。

下一步 ​

  • 编译渲染 - Shape 编译、运行时缓存与限制
  • 组件 - Component:Shape 的包装与高级用法
  • Compile API - Compile、Shape、Renderer 类参考

基于 MIT 许可证发布