Funkce hostitele
Každý uzávěr nebo volatelný objekt PHP předaný enginu přes setGlobal() se změní na volatelnou funkci JS. Není třeba nic ručně registrovat, definovat schéma ani provádět marshalling — stačí předat funkci.
Základy
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Když greet("world") běží v JS:
- Řetězec
"world"se převede z JS na PHP. - Zavolá se uzávěr PHP s převedeným argumentem.
- Návratová hodnota se převede z PHP zpět na JS.
Převod se řídí pravidly pro převod hodnot. Argumenty se kopírují: pole nebo objekt JS se předá jako pole PHP a jeho změna v PHP nezmění hodnotu JS.
Počet a parametry argumentů
Engine předá volatelné funkci JS argumenty pozičně a beze změny; její signaturu nezkoumá. Běžné uzávěry přebytečné argumenty ignorují a variadické parametry je shromáždí. Chybějící argumenty se nedoplňují: pokud JS předá méně argumentů, než volatelná funkce vyžaduje, PHP vyvolá ArgumentCountError. Nepovinným parametrům nastavte výchozí hodnoty.
$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
Přetypování
Argumenty se převádějí na PHP podle pravidel JS→PHP. Engine volá vaši funkci z kódu, který deklaruje strict_types=1, takže PHP striktně kontroluje typy skalárních parametrů a neprovádí automatickou konverzi: předání čísla typu JS parametru typu string je TypeError. Stejně jako každá PHP výjimka z hostitelské funkce se dostane k volajícímu eval(), nikoli do JS (viz níže).
$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
Buď proveďte konverzi ručně uvnitř uzávěru ((string) $value), nebo použijte sjednocené typy (int|string).
Předání výjimky zpět do JS
Běžné výjimky PHP vyvolané uvnitř hostitelské funkce se v současnosti nepřevádějí na chyby JS. Projdou všemi obslužnými rutinami JS try / catch a .catch() a dostanou se ke kódu PHP, který zavolal eval() nebo call(), a zachovají si původní třídu (issue #20). Patří sem \Exception, \RuntimeException, \DivisionByZeroError a \TypeError i \ArgumentCountError PHP vyvolané při chybných argumentech.
$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
}
Chcete-li vyvolat chybu, kterou může zachytit kód JS, vyvolejte některou z vlastních chybových tříd enginu: Phasis\Exceptions\TypeError, RangeError nebo ReferenceError se v JS projeví jako odpovídající typ chyby JS a každá jiná Phasis\Exceptions\RuntimeError jako prostá Error; v obou případech se zachová vaše zpráva.
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
}
');
Vrácení strukturovaných hodnot
Návratová hodnota se převede podle pravidel PHP→JS. Chcete-li vrátit objektový literál JS, vraťte asociativní pole 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);
');
Chcete-li vrátit pole JS, vraťte sekvenční seznam PHP:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
Vrácení objektu PHP, včetně příslibu PHP z ReactPHP, Amp nebo Guzzle, vede k JS undefined (viz Převod hodnot).
Asynchronní hostitelské funkce
Hostitelské funkce jsou synchronní. Příslib PHP nebo jiný objekt thenable se nevyčká: převede se na undefined, takže await hostFn() vyřeší hodnotu undefined. Proveďte práci synchronně a vraťte výsledek; kód JS může volání stále await, čímž hodnotu zabalí do vyřešeného příslibu:
$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();
');
Konkrétně pro HTTP vestavěný fetch() již vrací skutečný příslib JS a lze jej směrovat přes vašeho klienta PHP pomocí vlastního fetch transportu.
Poznámky k výkonu
Každé volání hostitelské funkce překračuje hranici PHP↔JS jednou pro argumenty a jednou pro návratovou hodnotu. Oba převody kopírují data, takže jejich náklady jsou O(size of value) — u primitiv jde o konstantní čas, u polí a objektů rostou úměrně jejich velikosti.
U často vykonávaných vnitřních smyček upřednostněte jedno volání, které vykoná více práce, před mnoha malými voláními. Nepředávejte sem a tam velká pole, když se mění jen několik položek.