Хост функции
Всяко затваряне или извикваем обект на 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(), със запазен оригинален клас (issue #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) — примитивите са с константно време, а масивите и обектите струват пропорционално на размера си.
При често изпълнявани вътрешни цикли предпочитайте едно извикване, което върши повече работа, пред много малки извиквания, и избягвайте да прехвърляте големи масиви напред-назад, когато се променят само няколко полета.