Funkcje hosta
Każde domknięcie lub funkcja wywoływalna PHP przekazana silnikowi przez setGlobal() staje się funkcją JS. Nie trzeba jej ręcznie rejestrować, tworzyć schematu ani marshallować — wystarczy przekazać funkcję.
Podstawy
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Gdy greet("world") działa w JS:
- Ciąg
"world"jest konwertowany z JS na PHP. - Domknięcie PHP jest wywoływane z przekonwertowanym argumentem.
- Wartość zwracana jest konwertowana z PHP z powrotem na JS.
Konwersja odbywa się zgodnie z zasadami konwersji wartości. Argumenty są kopiami: tablica lub obiekt JS trafia jako tablica PHP, a zmiana wprowadzona w PHP nie zmienia wartości JS.
Liczba argumentów i parametry
Silnik przekazuje funkcji wywoływalnej argumenty JS po kolei, bez zmian; nie analizuje jej sygnatury. Zwykłe domknięcia ignorują nadmiarowe argumenty, a parametry wariadyczne je zbierają. Brakujące argumenty nie są uzupełniane: jeśli JS przekaże ich mniej, niż wymaga funkcja, PHP zgłosi ArgumentCountError. Ustaw wartości domyślne dla parametrów opcjonalnych.
$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
Rzutowanie typów
Argumenty są konwertowane na PHP zgodnie z regułami JS→PHP. Silnik wywołuje przekazaną funkcję z kodu deklarującego strict_types=1, dlatego PHP ściśle sprawdza typy parametrów skalarnych i nie dokonuje konwersji: liczba JS przekazana do parametru string jest wartością TypeError. Podobnie jak każdy wyjątek PHP z funkcji hosta, trafia on do kodu wywołującego eval(), a nie JS (szczegóły poniżej).
$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
Możesz ręcznie konwertować wartość wewnątrz domknięcia ((string) $value) albo użyć typów sumy (int|string).
Zgłaszanie wyjątku z powrotem do JS
Zwykłe wyjątki PHP zgłaszane wewnątrz funkcji hosta nie są obecnie konwertowane na błędy JS. Przechodzą przez wszystkie procedury obsługi JS try / catch i .catch(), po czym trafiają do kodu PHP, który wywołał eval() lub call(), zachowując pierwotną klasę (zgłoszenie #20). Dotyczy to również \Exception, \RuntimeException, \DivisionByZeroError oraz wyjątków \TypeError i \ArgumentCountError PHP zgłaszanych przy nieprawidłowych argumentach.
$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
}
Aby zgłosić błąd przechwytywany przez kod JS, rzuć jedną z własnych klas błędów silnika: Phasis\Exceptions\TypeError, RangeError lub ReferenceError trafia do JS jako odpowiadający im typ błędu JS, a każdy inny Phasis\Exceptions\RuntimeError trafia jako zwykły Error; oba zawierają podany komunikat.
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
}
');
Zwracanie złożonych wartości
Wartość zwracana jest konwertowana zgodnie z regułami PHP→JS. Aby zwrócić literał obiektu JS, zwróć tablicę asocjacyjną 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);
');
Aby zwrócić tablicę JS, zwróć sekwencyjną listę PHP:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
Zwrócenie obiektu PHP, w tym obietnicy PHP z ReactPHP, Amp lub Guzzle, daje wartość JS undefined (zob. konwersja wartości).
Asynchroniczne funkcje hosta
Funkcje hosta działają synchronicznie. Obietnica PHP lub inny obiekt thenable nie jest oczekiwany: zostaje przekonwertowany na undefined, więc await hostFn() zwraca undefined. Wykonuj pracę synchronicznie i zwracaj wynik; kod JS nadal może wywołać await, co opakowuje wartość w rozwiązaną obietnicę:
$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();
');
W przypadku samego HTTP wbudowana funkcja fetch() już zwraca prawdziwą obietnicę JS i można skierować ją do własnego klienta PHP za pomocą własnego transportu fetch.
Uwagi dotyczące wydajności
Każde wywołanie funkcji hosta przekracza granicę PHP↔JS raz dla argumentów i raz dla wartości zwracanej. Obie konwersje kopiują dane, więc ich koszt jest O(size of value) — operacje na typach prostych mają stały czas, a koszt obsługi tablic i obiektów rośnie proporcjonalnie do ich rozmiaru.
W gorących pętlach wewnętrznych lepiej wykonać więcej pracy w jednym wywołaniu niż wiele małych wywołań. Unikaj też przesyłania dużych tablic tam i z powrotem, gdy zmienia się tylko kilka pól.