Przejdź do treści

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

PHP
$engine->setGlobal('greet', function (string $name): string {
    return "Hello, $name!";
});

echo $engine->eval('greet("world")');
// Hello, world!

Gdy greet("world") działa w JS:

  1. Ciąg "world" jest konwertowany z JS na PHP.
  2. Domknięcie PHP jest wywoływane z przekonwertowanym argumentem.
  3. 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.

PHP
$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).

PHP
$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.

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
}

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.

PHP
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:

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:

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ę:

PHP
$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.