Функції хоста
Будь-яке замикання або функція PHP, передана рушію через setGlobal(), стає функцією JS, яку можна викликати. Не потрібні ручна реєстрація, схема чи маршалізація — просто передайте функцію.
Основи
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Коли greet("world") виконується в JS:
- Рядок
"world"перетворюється з JS на PHP. - Викликається замикання PHP із перетвореним аргументом.
- Повернене значення перетворюється з PHP назад на JS.
Перетворення відбувається за правилами перетворення значень. Аргументи копіюються: масив або об’єкт JS надходить як масив PHP, і його зміни в PHP не змінюють значення JS.
Кількість аргументів і параметри
Рушій передає функції JS аргументи в заданому порядку без перевірки її сигнатури. Зайві аргументи звичайні замикання ігнорують, а варіативні параметри збирають. Відсутні аргументи не підставляються: якщо JS передає менше аргументів, ніж потрібно функції, PHP викидає ArgumentCountError. Для необов’язкових параметрів задавайте значення за замовчуванням.
$engine->setGlobal('format', function (string $template, ...$args): string {
return vsprintf($template, $args);
});
echo $engine->eval('format("%s is %d years old", "Ada", 36)');
// Ada is 36 years old
Приведення типів
Аргументи конвертуються в PHP за правилами JS→PHP. Рушій викликає вашу функцію з коду, у якому оголошено strict_types=1, тому PHP суворо перевіряє скалярні типи параметрів і не виконує приведення: число JS, передане параметру string, спричиняє TypeError. Як і будь-який виняток PHP із функції хост-середовища, він доходить до виклику eval(), а не до JS (див. нижче).
$engine->setGlobal('len', fn(string $s): int => strlen($s));
$engine->eval('len(42)'); // PHP TypeError: Argument #1 ($s) must be of type string, int given
Або виконайте приведення вручну всередині замикання ((string) $value), або використайте об’єднані типи (int|string).
Передавання винятків назад у JS
Звичайні винятки PHP, викинуті всередині функції хост-середовища, наразі не перетворюються на помилки JS. Вони проходять крізь усі обробники JS try / catch і .catch() та доходять до коду PHP, який викликав eval() або call(), з початковим класом (помилка №20). Це стосується \Exception, \RuntimeException, \DivisionByZeroError, а також помилок \TypeError і \ArgumentCountError PHP, що виникають через некоректні аргументи.
$engine->setGlobal('divide', function (int $a, int $b): int {
if ($b === 0) {
throw new \DivisionByZeroError('cannot divide by zero');
}
return intdiv($a, $b);
});
try {
$engine->eval('
try { divide(10, 0); } catch (e) { /* never runs */ }
');
} catch (\DivisionByZeroError $e) {
echo $e->getMessage(); // cannot divide by zero
}
Щоб викинути помилку, яку може перехопити код JS, викиньте один із власних класів помилок рушія: Phasis\Exceptions\TypeError, RangeError або ReferenceError надходять у JS як відповідний тип помилки JS, а будь-який інший Phasis\Exceptions\RuntimeError надходить як звичайний Error; у кожному разі з вашим повідомленням.
use Phasis\Exceptions\RangeError;
$engine->setGlobal('divide', function (int $a, int $b): int {
if ($b === 0) {
throw new RangeError('cannot divide by zero');
}
return intdiv($a, $b);
});
$engine->eval('
try {
divide(10, 0);
} catch (e) {
console.log(e.name, e.message); // RangeError cannot divide by zero
}
');
Повернення складних значень
Повернене значення конвертується за правилами PHP→JS. Щоб повернути літерал об’єкта JS, поверніть асоціативний масив PHP:
$engine->setGlobal('lookup', function (string $key): array {
return ['key' => $key, 'value' => 42, 'meta' => ['ts' => time()]];
});
$engine->eval('
const r = lookup("answer");
console.log(r.value, r.meta.ts);
');
Щоб повернути масив JS, поверніть послідовний список PHP:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
Повернення об’єкта PHP, зокрема промісу PHP із ReactPHP, Amp або Guzzle, спричиняє JS undefined (див. Перетворення значень).
Асинхронні функції хост-середовища
Функції хост-середовища синхронні. Проміс PHP або інший thenable-об’єкт не очікується: він конвертується в undefined, тож await hostFn() повертає undefined. Виконайте роботу синхронно й поверніть результат; код JS усе одно може await виклик, обгорнувши значення у виконаний проміс:
$engine->setGlobal('loadBody', function (string $url): string {
return file_get_contents($url);
});
$engine->eval('
async function main() {
const body = await loadBody("https://example.com");
console.log(body.length);
}
main();
');
Для HTTP вбудований fetch() уже повертає справжній проміс JS, тож його можна спрямувати через власний клієнт PHP із власним транспортом fetch.
Примітки щодо продуктивності
Кожен виклик функції хост-середовища один раз перетинає межу PHP↔JS для аргументів і ще раз для поверненого значення. Обидва перетворення копіюють дані, тому їхня складність становить O(size of value): примітиви обробляються за сталий час, а вартість для масивів і об’єктів пропорційна їхньому розміру.
Для гарячих внутрішніх циклів краще виконати більше роботи за один виклик, ніж робити багато дрібних викликів, і не передавати великі масиви туди й назад, якщо змінюється лише кілька полів.