Treci la conținut

Funcții gazdă

Orice funcție de tip closure sau funcție apelabilă PHP transmisă motorului prin setGlobal() devine o funcție apelabilă JS. Nu este necesară înregistrarea manuală, nu există schemă sau marshalling — transmiteți pur și simplu funcția apelabilă.

Noțiuni de bază

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

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

Când greet("world") rulează în JS:

  1. Șirul "world" este convertit din JS în PHP.
  2. Funcția closure PHP este apelată cu argumentul convertit.
  3. Valoarea returnată este convertită din PHP înapoi în JS.

Conversia urmează regulile de conversie a valorilor. Argumentele sunt copii: un tablou sau obiect JS ajunge ca tablou PHP, iar modificarea lui în PHP nu schimbă valoarea JS.

Numărul și parametrii argumentelor

Motorul transmite argumentele JS funcției apelabile, în ordine și așa cum sunt; nu îi inspectează semnătura. Funcțiile closure obișnuite ignoră argumentele suplimentare, iar parametrii variadici le colectează. Argumentele lipsă nu sunt completate: dacă JS transmite mai puține argumente decât cere funcția apelabilă, PHP aruncă ArgumentCountError. Atribuiți valori implicite parametrilor opționali.

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

Conversia tipurilor

Argumentele sunt convertite în PHP conform regulilor JS→PHP. Motorul apelează funcția folosind cod care declară strict_types=1, astfel că PHP verifică strict tipurile parametrilor scalari și nu face conversii implicite: un număr JS transmis unui parametru string este TypeError. Ca orice excepție PHP dintr-o funcție gazdă, aceasta ajunge la apelantul lui eval(), nu la JS (vedeți mai jos).

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

Fie convertiți manual în interiorul funcției closure ((string) $value), fie folosiți tipuri union (int|string).

Aruncarea excepțiilor înapoi în JS

Excepțiile obișnuite PHP aruncate în interiorul unei funcții gazdă nu sunt convertite în erori JS în prezent. Ele trec prin toate gestionările JS try / catch și .catch(), apoi ajung la codul PHP care a apelat eval() sau call(), păstrându-și clasa inițială (problema nr. 20). Sunt incluse \Exception, \RuntimeException, \DivisionByZeroError și erorile PHP \TypeError și \ArgumentCountError pentru argumente invalide.

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
}

Pentru a arunca o eroare pe care codul JS o poate captura, aruncați una dintre clasele de erori proprii motorului: Phasis\Exceptions\TypeError, RangeError sau ReferenceError ajung în JS ca tipul de eroare JS corespunzător, iar orice altă Phasis\Exceptions\RuntimeError ajunge ca un Error simplu, fiecare cu mesajul transmis de dumneavoastră.

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
    }
');

Returnarea valorilor complexe

Valoarea returnată este convertită conform regulilor PHP→JS. Pentru a returna un obiect literal JS, returnați un tablou asociativ 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);
');

Pentru a returna un tablou JS, returnați o listă secvențială 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

Returnarea unui obiect PHP, inclusiv a unei promisiuni PHP de la ReactPHP, Amp sau Guzzle, produce JS undefined (vedeți Conversia valorilor).

Funcții gazdă asincrone

Funcțiile gazdă sunt sincrone. O promisiune PHP sau un alt obiect thenable nu este așteptat: se convertește în undefined, astfel că await hostFn() se rezolvă cu undefined. Efectuați operația sincron și returnați rezultatul; codul JS poate totuși să apeleze cu await, ceea ce încadrează valoarea într-o promisiune rezolvată:

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();
');

În cazul HTTP, funcția încorporată fetch() returnează deja o promisiune reală JS și poate fi direcționată prin propriul client PHP cu un transport fetch personalizat.

Note despre performanță

Fiecare apel al unei funcții gazdă traversează o dată limita PHP↔JS pentru argumente și o dată pentru valoarea returnată. Ambele conversii fac copii, deci costurile sunt O(size of value) — operațiile cu primitive au timp constant, iar cele cu tablouri și obiecte sunt proporționale cu dimensiunea lor.

În buclele interne foarte solicitate, preferați un singur apel care efectuează mai multă muncă în locul multor apeluri mici și evitați să transmiteți tablouri mari înainte și înapoi când se modifică doar câteva câmpuri.