TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

8.8 KiB

将 PHP 依赖嵌入可执行文件

embedded-files 用于把 Composer 依赖、PHP fallback 文件和运行时资源打包进 TypePHP 可执行文件。它主要解决以下部署问题:

  • 第三方库不支持 TypePHP AOT,无法放入 sources 原生编译;
  • 程序仍需通过 Composer autoload 按需加载这些 PHP 类;
  • 发布环境不希望执行 composer install,也不希望携带独立的 vendor 目录;
  • 配置、模板、证书等只读文件也需要随程序一起发布。

启用后,TypePHP 在构建期将不能 AOT 编译的 PHP 文件转换为 OPcache opcode, 同时把配置中选中的全部文件写入可执行文件。运行时 require、require_once 和 Composer autoload 直接从内存中的 opcode 表加载 PHP 文件,普通只读文件则通过 内存文件表读取。

推荐用法

开发构建不启用 embedded-files,直接使用工作区中的 vendor。发布构建通过 YAML include 复用公共配置,并只在发布阶段嵌入依赖。

project.yml:

name: myapp
mode: bin
build-dir: build

sources:
  - app.php
  - src

ignore:
  - src/legacy

project-release.yml:

include: project.yml
optimize: 2

embedded-files:
  - vendor
  - resources/config

构建发布版本:

composer install --no-dev --classmap-authoritative
tpc project-release.yml

项目根目录的 app.php 仍按普通 Composer 方式启动自动加载器:

<?php

function main(): void
{
    require_once __DIR__ . '/vendor/autoload.php';

    $application = new App\Application();
    $application->run();
}

生成的可执行文件可以移到没有项目源码和 vendor 目录的位置运行:

mkdir -p /tmp/myapp-release
cp myapp /tmp/myapp-release/
cd /tmp/myapp-release
./myapp

这里的“独立运行”是指不再需要 PHP CLI、Composer 安装和磁盘上的 PHP 项目文件。 程序仍需要构建方式所对应的 libphp、PHPX 及其他原生共享库;这些依赖需随发布包 提供,或使用项目支持的静态链接方式处理。

sources、ignore 和 embedded-files 的关系

三个配置项承担不同职责:

配置 作用
sources 选择要翻译为 C++ 并编译为机器码的 PHP/C/C++ 文件。
ignore 从 sources 的扫描结果中排除文件。
embedded-files 选择要原样打包进可执行文件的文件,并为没有成功 AOT 编译的 PHP 文件生成 opcode。

一个文件可以同时由 sources 和 embedded-files 选中:

  • 成功进入 AOT 的 PHP 文件使用原生实现,不再生成重复 opcode;
  • 被 ignore 排除或因不支持的语法而未进入 AOT 的 PHP 文件会进入 opcode 表;
  • ignore 不会从 embedded-files 中删除文件;
  • .stub.php 只作为 API 声明原样打包,不会生成可执行 opcode。

因此可以先让应用代码进入 sources,再将整个 vendor 放入 embedded-files。支持 AOT 的文件继续以机器码运行,其余依赖由 ZendVM 按需执行。

sources:
  - src
  - vendor/acme/optimized-package/src

embedded-files:
  - vendor

embedded-files 没有单独的排除列表。若不希望打包某个子目录,应列出实际需要的 目录或文件,而不是配置整个上级目录。

支持的配置格式

该配置只在 YAML 项目中显式启用,值必须是列表。每一项可以是文件、目录或带条件的 路径,相对路径以最外层项目 YAML 所在目录为基准。

embedded-files:
  - vendor
  - resources/app.json
  - path: resources/windows
    if: PHP_OS_FAMILY == "Windows"
  - path: resources/php85
    if: PHP_VERSION_ID >= 80500

目录会被递归扫描,其中的所有普通文件都会进入归档。除 PHP 文件外,JSON、YAML、 模板和其他资源也会按原始字节保存。

运行时可以使用原来的路径读取已嵌入文件:

$config = file_get_contents(__DIR__ . '/resources/app.json');

内嵌文件是只读的。程序产生的日志、缓存、上传文件和数据库应写入单独的运行时数据 目录。不要依赖对内嵌目录执行 glob() 或目录遍历;应使用已知文件路径读取资源。

构建环境要求

embedded-files 仅支持普通的 mode: bin 构建。它不适用于 ext、lib、Nano、 WASI、iOS 或 Android 目标。

构建机必须具备:

  1. 与目标 libphp 匹配的 php 或 php.exe;
  2. 可由该 CLI 加载的 Zend OPcache 扩展;
  3. 与最终运行时一致的 PHP 完整版本、ZTS/NTS、Debug 模式和整数宽度。

可以先检查构建 PHP:

php -r 'var_dump(PHP_VERSION, PHP_ZTS, extension_loaded("Zend OPcache"), function_exists("opcache_compile_file"));'

TypePHP 自己也会用 -n 探测 OPcache,并尝试加载标准位置中的扩展。仅有 tpc 可执行文件但没有匹配的 PHP CLI 和 OPcache 时,无法构建 embedded-files。

OPcache 只参与构建期序列化。运行生成的程序时:

  • 不需要启用或安装 OPcache 扩展;
  • 不需要 PHP CLI;
  • 不需要执行 composer install;
  • 不需要磁盘上的 vendor/autoload.php 或其他已嵌入文件。

二进制中的 opcode 与 PHP 完整版本绑定。若运行时 PHP 与生成 opcode 的 PHP 版本 不同,程序会在启动时报告版本不匹配,而不会继续执行不兼容的字节码。

构建输出和日志

首次构建会看到类似输出:

embedded-files: found 3012 files (2886 PHP)
Generating embedded opcodes for 2886 PHP files
Vendor opcode cache: 0 reused, 2886 to generate
Packed 3012 files and 2886 opcode blobs

再次构建时,满足缓存条件的 Composer vendor opcode 会被复用:

Vendor opcode cache: 2886 reused, 0 to generate
Packed 3012 files and 2886 opcode blobs

内部 opcode、归档和对象文件位于 build-dir/cache。生成的 embedded-opcodes-<name>.cc 只包含可读的索引代码,大块文件内容不会展开成 C++ 数组。

OPcache 无法编译且预期不会被 require 的 PHP 文件会显示 Skipping non-executable embedded PHP file,文件原始内容仍会被打包。若程序实际会 加载该文件,应修复构建错误,不能忽略这条日志。

vendor opcode 缓存

只有目录名为 vendor 且根目录存在 autoload.php 时,TypePHP 才启用持久 opcode 缓存。缓存键包含:

  • vendor 根目录路径和目录 mtime;
  • PHP CLI 与 OPcache 二进制签名;
  • PHP 版本、ZTS/Debug 模式及整数宽度。

其他 embedded-files 目录每次构建都会重新生成 opcode,因为编译器无法确定它们的 可靠失效边界。

直接修改 vendor 中一个已有文件不一定改变 vendor 根目录 mtime。此时使用 --force 重新生成:

tpc project-release.yml --force

Composer install 或 update 通常会重建根目录内容和 autoload 文件,但发布流程仍应 在依赖发生人工修改或缓存结果可疑时使用 --force。

性能和发布建议

嵌入大量 vendor 文件会增大最终二进制、链接输入和进程启动成本。对于频繁启动大量 短进程的 PHPT、单元测试或本地开发,这部分固定成本会很明显。因此建议:

  • 开发和测试使用不含 embedded-files 的 project.yml;
  • 只在交付构建使用 project-release.yml;
  • 保留同一个 build-dir 以复用 opcode、归档和对象缓存;
  • 修改 vendor 内容但缓存未失效时再使用 --force。

Composer autoload 仍是懒加载:只有代码首次引用某个类时,对应 vendor opcode 才由 ZendVM 执行。打包整个 vendor 不会在程序启动时执行全部 PHP 文件。

常见问题

为什么配置后仍然需要 Composer autoload?

embedded-files 改变的是文件保存位置和 PHP 文件的编译来源,不会替代 Composer 生成的类映射与 PSR-4 规则。程序仍然 require_once 同一个 vendor/autoload.php,只是该文件和后续类文件都从二进制内存表加载。

能否只把 vendor/autoload.php 放进去?

不能。autoload 文件只包含加载规则,真正的包文件也必须存在于 embedded-files 中。通常应直接配置整个 vendor。

运行主机需要 OPcache 吗?

不需要。反序列化和执行支持已经随 PHPX 链入程序。OPcache 扩展只用于构建 opcode。

这是否能保护 PHP 源码?

不要把它当作加密或源码保护功能。为了支持普通文件读取,归档中包含所选文件的原始 字节;能够分析二进制文件的人仍可能提取这些内容。

vendor 代码依赖其他 PHP 扩展怎么办?

embedded-files 只打包 PHP 文件,不会把 curl、pdo_mysql 等扩展实现打进程序。 应使用 ext-deps / extension-dependencies 声明依赖,并在目标运行时提供对应扩展。

ext-deps:
  - curl
  - pdo_mysql