宿主函数
通过 setGlobal() 交给引擎的任何 PHP 闭包或可调用对象,都会成为可调用的 JS 函数。无需手动注册、定义架构或编组——只需传入可调用对象。
基础知识
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
当 greet("world") 在 JS 中运行时:
- 字符串
"world"会从 JS 转换为 PHP。 - PHP 闭包会使用转换后的参数调用。
- 返回值会从 PHP 转回 JS。
转换遵循 值转换规则。参数是副本:JS 数组或对象会作为 PHP 数组传入,在 PHP 中修改它不会更改 JS 中的值。
参数数量与参数
引擎会按位置将 JS 参数原样传给可调用对象;它不会检查签名。普通闭包会忽略多余参数,可变参数会收集这些参数。缺少的参数不会自动补齐:如果 JS 传入的参数少于可调用对象所需数量,PHP 会抛出 ArgumentCountError。请为可选参数提供默认值。
$engine->setGlobal('format', function (string $template, ...$args): string {
return vsprintf($template, $args);
});
echo $engine->eval('format("%s is %d years old", "Ada", 36)');
// Ada is 36 years old
类型转换
参数会按照 JS→PHP 规则转换为 PHP。引擎从声明了 strict_types=1 的代码调用你的可调用对象,因此 PHP 会严格检查标量参数类型,不会进行强制转换:将 JS 数值传给 string 参数会导致 TypeError。与宿主函数抛出的任何 PHP 异常一样,它会传递给 eval() 的调用方,而不是 JS(见下文)。
$engine->setGlobal('len', fn(string $s): int => strlen($s));
$engine->eval('len(42)'); // PHP TypeError: Argument #1 ($s) must be of type string, int given
可以在闭包中手动转换((string) $value),也可以使用联合类型(int|string)。
将异常传回 JS
如今,在宿主函数中抛出的普通 PHP 异常不会转换为 JS 错误。它们会经过所有 JS try / catch 和 .catch() 处理程序,保持原有类别传到调用 eval() 或 call() 的 PHP 代码(问题 #20)。其中包括 \Exception、\RuntimeException、\DivisionByZeroError,以及 \TypeError 和 \ArgumentCountError 对错误参数抛出的 PHP。
$engine->setGlobal('divide', function (int $a, int $b): int {
if ($b === 0) {
throw new \DivisionByZeroError('cannot divide by zero');
}
return intdiv($a, $b);
});
try {
$engine->eval('
try { divide(10, 0); } catch (e) { /* never runs */ }
');
} catch (\DivisionByZeroError $e) {
echo $e->getMessage(); // cannot divide by zero
}
要抛出可由 JS 代码捕获的错误,请抛出引擎自带的错误类之一:Phasis\Exceptions\TypeError、RangeError 或 ReferenceError 会在 JS 中作为对应的 JS 错误类型出现;其他任何 Phasis\Exceptions\RuntimeError 都会作为普通的 Error 出现,并保留你提供的消息。
use Phasis\Exceptions\RangeError;
$engine->setGlobal('divide', function (int $a, int $b): int {
if ($b === 0) {
throw new RangeError('cannot divide by zero');
}
return intdiv($a, $b);
});
$engine->eval('
try {
divide(10, 0);
} catch (e) {
console.log(e.name, e.message); // RangeError cannot divide by zero
}
');
返回复杂值
返回值按照 PHP→JS 规则转换。若要返回 JS 对象字面量,请返回 PHP 关联数组:
$engine->setGlobal('lookup', function (string $key): array {
return ['key' => $key, 'value' => 42, 'meta' => ['ts' => time()]];
});
$engine->eval('
const r = lookup("answer");
console.log(r.value, r.meta.ts);
');
若要返回 JS 数组,请返回顺序排列的 PHP 列表:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
返回 PHP 对象(包括来自 ReactPHP、Amp 或 Guzzle 的 PHP promise)会产生 JS undefined(参见值转换)。
异步宿主函数
宿主函数是同步的。PHP promise 或其他 thenable 对象不会被等待:它会转换为 undefined,因此 await hostFn() 会解析为 undefined。请同步完成工作并返回结果;JS 代码仍可对该调用执行 await,从而将值包装在一个已解析的 promise 中:
$engine->setGlobal('loadBody', function (string $url): string {
return file_get_contents($url);
});
$engine->eval('
async function main() {
const body = await loadBody("https://example.com");
console.log(body.length);
}
main();
');
对于 HTTP,内置的 fetch() 已经会返回真正的 JS promise,并且可以通过 自定义 fetch 传输,路由到你自己的 PHP 客户端。
性能说明
每次调用宿主函数时,参数和返回值各跨越一次 PHP↔JS 边界。两次转换都会复制数据,因此复杂度为 O(size of value)——基本值的耗时是常数,数组和对象的成本则与其大小成正比。
对于频繁执行的内层循环,最好一次调用完成更多工作,而不是多次进行小调用;如果只更改少数几个字段,也应避免来回传递大型数组。