Функции хоста
Любое замыкание или вызываемый объект 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): для примитивов она постоянна, а для массивов и объектов пропорциональна их размеру.
Для часто выполняемых внутренних циклов предпочитайте один вызов, выполняющий больше работы, множеству небольших вызовов. Не передавайте большие массивы туда и обратно, если меняются лишь несколько полей.