Начало работы
Установка
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 также необязателен: для шифрования AES-CBC, AES-CTR и AES-GCM в crypto.subtle он необходим, а дайджесты и 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; значение последнего выражения он не выводит.
Полный список команд см. в справочнике CLICLI.
Встраивание в 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 и всё остальное незаметно создаются при первом чтении. Передайте new Engine(eager: true), чтобы установить все встроенные компоненты при создании — это полезно для инструментов проверки соответствия, которые анализируют дескрипторы до чтения; при обычном встраивании требуется редко.
Вызов функций JS из PHP
$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. При вызове setGlobal() с обычным объектом PHP привязывает undefined (отслеживается в проблеме #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) — единственное ограничение, настраиваемое во время выполнения; сейчас оно применяется только к коду, который запускает интерпретатор с обходом дерева. Циклы, выполняемые виртуальной машиной байткода (тела функций и большая часть кода верхнего уровня), не учитываются, поэтому одно это ограничение не остановит while (true) {} (проблема #19). При достижении лимита ошибку можно перехватить в JS; если её не перехватить, она попадет в PHP как Phasis\Exceptions\InternalError, подкласс Phasis\Exceptions\RuntimeError.
У движка также есть фиксированный предел стека вызовов в 4,096 кадров (для транспилированных замыканий действует ограничение в 8,192 кадров); при превышении выбрасывается ошибка «Maximum call stack size exceeded», снова как InternalError. Ограничения по реальному времени нет, поэтому запускайте недоверенный код с собственным max_execution_time среды PHP или в отдельном процессе. См. раздел APIsetLimit.
Следующие шаги
- APIСправочник CLI — полный интерфейс
Phasis\Engine. - Взаимодействие — правила преобразования значений, функции хоста, объекты PHP.
- Совместимость — реализованные возможности, охват test262.
- Парсер и Форматтер — разбор и форматирование JavaScript без его выполнения.
- Дополнительно — архитектура, виртуальная машина байткода, тесты производительности, проверка по эталону.