Перейти до вмісту

Початок роботи

Встановлення

Командна оболонка
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) і календарі 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
<?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 або в окремому процесі. Див. API: setLimit.

Наступні кроки