Початок роботи
Встановлення
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) і календарі Temporal, що не належать до ISO (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; значення останнього виразу не виводиться.
Повний перелік команд див. у довіднику CLI.
Вбудовування в 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 або в окремому процесі. Див. API: setLimit.
Наступні кроки
- API довідник — повний API
Phasis\Engine. - Взаємодія — правила перетворення значень, функції хоста, об’єкти PHP.
- Сумісність — реалізовані можливості, покриття test262.
- Парсер і Форматувальник — розбір і форматування JavaScript без виконання коду.
- Додаткові теми — архітектура, віртуальна машина байткоду, бенчмарки, тестування за допомогою оракула.