快速入门
安装
composer require phasis/phasis
Phasis 需要 PHP 8.2 或更高版本,以及三个扩展;它们都是 composer.json 所必需的:
mbstring— 字符串处理。bcmath— BigInt 运算和整数精度数值处理。curl—fetch()背后的默认传输机制。
mbstring 和 curl 在大多数构建中均已启用。bcmath 通常未启用:官方 php Docker 镜像(php:8.x-cli、php:8.x-fpm)未包含它,有些发行版则将其单独打包(php-bcmath)。缺少它时,Composer 会拒绝安装 Phasis。如果 Dockerfile 基于官方镜像,请添加:
RUN docker-php-ext-install bcmath
运行 php -m,查看你的构建启用了哪些扩展。
ext-intl 是可选的。Phasis 即使没有它也能运行,但整个 Intl.* 系列(Collator、NumberFormat、DateTimeFormat、PluralRules、Locale、DisplayNames、ListFormat、RelativeTimeFormat、Segmenter、DurationFormat)以及非 ISO 的 Temporal 日历(hebrew、islamic、japanese、persian 等)都需要它。只要宿主应用提供涉及区域设置格式化的 JS,就应安装它。ext-openssl 也是可选的:crypto.subtle 的 AES-CBC、AES-CTR 和 AES-GCM 加密需要它,而摘要和 HMAC 无需它也能运行。
无需 exec,无需 FFI,无需 Node.js,也无需自定义原生扩展。
从 CLI 运行脚本
./vendor/bin/phasis -e 'console.log(1 + 2 * 3)'
# 7
./vendor/bin/phasis -e 'console.log([1, 2, 3].map(x => x * x).join(","))'
# 1,4,9
./vendor/bin/phasis path/to/script.js
CLI 会输出脚本通过 console.log 写入的内容;它不会输出最后一个表达式的值。
完整命令集请参阅 CLI 命令参考。
在 PHP 中嵌入
<?php
require __DIR__ . '/vendor/autoload.php';
use Phasis\Engine;
$engine = new Engine();
// Evaluate an expression
$result = $engine->eval('1 + 2 * 3');
echo $result;
// 7
// Execute a script file
$engine->execFile('path/to/script.js');
// Bridge a PHP value into the JS global scope
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->eval('console.log(config.version)');
// console output is buffered on the engine, not printed
echo $engine->getConsoleOutput();
// 1.0
Engine 实例会在多次调用之间保留其全局状态,因此后续调用 eval() 时可以使用之前定义的内容。
console.log 及其同类方法会将内容追加到引擎的缓冲区,而不是写入 stdout。可通过 getConsoleOutput()(一个字符串,各行以 \n 连接)或 getConsoleLines()(行列表)读取,并通过 clearConsole() 清空。
冷启动开销很低:仅预先安装规范加载所需的核心部分(Object、Function、Array、Promise、Symbol、BigInt、RegExp 和错误类型)。Map、TypedArray、Temporal、Intl、URL、fetch、完整的 Web Platform / Fetch Packs 以及其他所有内容,都会在首次读取时透明地实例化。构造时传入 new Engine(eager: true) 可安装所有内置项——适用于在读取描述符之前检查它们的一致性工具;常规嵌入通常无需此选项。
从 PHP 调用 JS 函数
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);
// 5
call() 会按名称在全局对象上查找函数,并使用提供的 PHP 参数调用它(参数会自动转换为 JS 值)。
将可调用的 PHP 暴露为 JS 函数
$engine->setGlobal('fetchData', function (string $url): array {
return json_decode(file_get_contents($url), true);
});
$engine->eval('const users = fetchData("https://api.example.com/users")');
PHP 闭包会被包装为 JS 函数。调用时,参数会从 JS 转换为 PHP,返回值则会从 PHP 转换回 JS。
PHP 对象
PHP 对象目前还不能与 JavaScript 共享。将普通 PHP 对象传给 setGlobal() 会绑定 undefined(相关问题见 issue #18)。请将数据作为数组传递;数组会被复制到 JS 中,并将行为作为闭包暴露:
class Counter {
public int $value = 0;
public function increment(): void { $this->value++; }
}
$counter = new Counter();
$engine->setGlobal('increment', fn () => $counter->increment());
$engine->eval('increment(); increment();');
echo $counter->value;
// 2
详情及其他变通方法请参阅 共享对象。
资源限制
$engine = new Engine();
$engine->setLimit('maxLoopIterations', 100_000);
maxLoopIterations(默认值为 100,000)是唯一可在运行时调整的限制,目前仅适用于树遍历解释器运行的代码。字节码 VM 运行的循环(函数体和大多数顶层代码)不计入限制,因此仅靠这一限制无法阻止 while (true) {}(issue #19)。达到限制时,可在 JS 中捕获错误;若未捕获,错误会以 Phasis\Exceptions\InternalError 的形式传递到 PHP,它是 Phasis\Exceptions\RuntimeError 的子类。
引擎还设有固定的 4,096 帧调用栈上限(转译闭包另有 8,192 帧保护);超过上限会抛出“Maximum call stack size exceeded”,同样属于 InternalError。引擎没有墙钟时间限制,因此请在 PHP 自身的 max_execution_time 中或单独进程里运行不可信代码。参阅 API:setLimit。