コンテンツへスキップ

はじめに

インストール

シェル
composer require phasis/phasis

composer.jsonが必要とするPhasisの要件は、PHP 8.2以降と、次の3つの拡張機能です:

  • mbstring — 文字列処理。
  • bcmath — BigInt演算と整数精度の数値処理。
  • curl — fetch()のデフォルトのトランスポート。

mbstringとcurlは、ほとんどのビルドで有効です。一方、bcmathは有効でないことがよくあります。公式のphp Dockerイメージ(php:8.x-cli、php:8.x-fpm)には含まれず、一部のディストリビューションでは別パッケージになっています(php-bcmath)。これがないとComposerはPhasisのインストールを拒否します。公式イメージをベースにしたDockerfileでは、次を追加してください:

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の暗号化には必要ですが、digestと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
<?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とその関連メソッドは、標準出力に書き込む代わりにエンジン上のバッファへ追記します。getConsoleOutput()(1つの文字列として、各行を\nで連結)またはgetConsoleLines()(行のリスト)で読み取り、clearConsole()で空にします。

コールドスタートの負荷は軽く、仕様の読み込みに必要なコア(Object、Function、Array、Promise、Symbol、BigInt、RegExp、エラー型)のみが最初にインストールされます。Map、TypedArray、Temporal、Intl、URL、fetch、Web Platform Pack / Fetch Pack全体、およびその他の機能は、初回読み込み時に透過的に展開されます。構築時にすべての組み込み機能をインストールするにはnew Engine(eager: true)を渡します。読み込む前に記述子を調べる適合性ツールに便利ですが、通常の埋め込みではほとんど必要ありません。

PHPからJS関数を呼び出す

PHP
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);
// 5

call()はグローバルオブジェクト上で名前から関数を検索し、指定されたPHP引数(自動的にJS値へ変換)を渡して呼び出します。

PHPの呼び出し可能オブジェクトをJS関数として公開する

PHP
$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へコピーされる配列として渡し、動作はクロージャとして公開してください。

PHP
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

詳細とその他の回避策については、共有オブジェクトをご覧ください。

リソース上限

PHP
$engine = new Engine();
$engine->setLimit('maxLoopIterations', 100_000);

maxLoopIterations(デフォルトは100,000)は実行時に変更できる唯一の上限で、現在はツリーウォーキングインタープリターが実行するコードにのみ適用されます。バイトコードVMが実行するループ(関数本体とトップレベルコードの大部分)はカウントされないため、この上限だけではwhile (true) {}を止められません(issue #19)。上限に達したときのエラーはJSで捕捉できます。捕捉されない場合はPHPにPhasis\Exceptions\InternalErrorとして伝わり、Phasis\Exceptions\RuntimeErrorのサブクラスです。

エンジンには固定のコールスタック上限4,096フレーム(トランスパイルされたクロージャ用のガードは8,192フレーム)もあり、超えると「Maximum call stack size exceeded」がInternalErrorとしてスローされます。実時間の制限はないため、信頼できないコードはPHP独自のmax_execution_time内か、別プロセスで実行してください。API: setLimitをご覧ください。

次のステップ