Перейти к содержимому

Начало работы

Установка

Командная оболочка
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 на основе официального образа добавьте:

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
<?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

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. При вызове setGlobal() с обычным объектом PHP привязывает undefined (отслеживается в проблеме #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) — единственное ограничение, настраиваемое во время выполнения; сейчас оно применяется только к коду, который запускает интерпретатор с обходом дерева. Циклы, выполняемые виртуальной машиной байткода (тела функций и большая часть кода верхнего уровня), не учитываются, поэтому одно это ограничение не остановит 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.

Следующие шаги