ホスト関数
setGlobal()を介してエンジンに渡されたPHPのクロージャまたは呼び出し可能オブジェクトは、呼び出し可能なJS関数になります。手動登録もスキーマもマーシャリングも不要で、呼び出し可能オブジェクトを渡すだけです。
基本
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
JSでgreet("world")を実行すると:
- 文字列
"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例外と同様に、JSではなくeval()の呼び出し元に届きます(後述)。
$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コードに届きます(issue #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のPHPpromiseを含め、JSではundefinedになります(値の変換を参照)。
非同期ホスト関数
ホスト関数は同期的です。PHPのpromiseなどのthenableオブジェクトはawaitされず、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境界を1回ずつ越えます。どちらの変換もコピーを行うため、コストはO(size of value)です。プリミティブは一定時間で処理され、配列とオブジェクトはサイズに比例したコストがかかります。
頻繁に実行される内側のループでは、細かな呼び出しを何度も行うより、1回の呼び出しで多くの処理を行う方が効率的です。また、変更するフィールドが少ない場合は、大きな配列を何度も受け渡さないようにしてください。