Siirry sisältöön

Isäntäfunktiot

Mikä tahansa PHP-sulkeuma tai kutsuttava, joka välitetään moottorille setGlobal()-määrityksellä, muuttuu kutsuttavaksi JS-funktioksi. Manuaalista rekisteröintiä, skeemaa tai marshaling-vaihetta ei tarvita — välitä vain kutsuttava.

Perusteet

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

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

Kun greet("world") suoritetaan JS-ympäristössä:

  1. Merkkijono "world" muunnetaan muodosta JS muotoon PHP.
  2. PHP-sulkeumaa kutsutaan muunnetulla argumentilla.
  3. Paluuarvo muunnetaan muodosta PHP takaisin muotoon JS.

Muunnos noudattaa arvojen muuntosääntöjä. Argumentit ovat kopioita: JS-taulukko tai -olio saapuu PHP-taulukkona, eikä sen muuttaminen PHP-ympäristössä muuta JS-arvoa.

Argumenttien määrä ja parametrit

Moottori välittää kutsuttavalle JS-argumentit järjestyksessä sellaisinaan; se ei tutki allekirjoitusta. Tavalliset sulkeumat ohittavat ylimääräiset argumentit, ja variadiset parametrit keräävät ne. Puuttuvia argumentteja ei täydennetä: jos JS välittää vähemmän argumentteja kuin kutsuttava vaatii, PHP heittää ArgumentCountError-virheen. Aseta valinnaisille parametreille oletusarvot.

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

Tyyppimuunnokset

Argumentit muunnetaan muotoon PHP käyttäen sääntöjä JS→PHP. Moottori kutsuu kutsuttavaasi koodista, joka määrittää strict_types=1-asetuksen, joten PHP tarkistaa skalaariarvoisten parametrien tyypit tiukasti eikä tee pakottavia muunnoksia: JS-luvun välittäminen string-parametrille aiheuttaa TypeError-virheen. Kuten mikä tahansa PHP-poikkeus isäntäfunktiosta, se päätyy eval():n kutsujalle eikä JS:lle (katso alla).

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

Muunna arvo käsin sulkeuman sisällä ((string) $value) tai käytä union-tyyppejä (int|string).

Poikkeusten välittäminen takaisin kohteeseen JS

Tavallisia PHP-poikkeuksia, jotka heitetään isäntäfunktion sisällä, ei tällä hetkellä muunneta JS-virheiksi. Ne kulkevat kaikkien JS try- ja catch- sekä .catch()-käsittelijöiden läpi ja saapuvat PHP-koodiin, joka kutsui eval()- tai call()-toimintoa, alkuperäinen luokkansa säilyttäen (ongelma #20). Tämä koskee myös \Exception-, \RuntimeException- ja \DivisionByZeroError-virheitä sekä huonoista argumenteista johtuvia \TypeError- ja \ArgumentCountError-PHP-virheitä.

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
}

Jos haluat heittää virheen, jonka JS-koodi voi siepata, heitä jokin moottorin omista virheluokista: Phasis\Exceptions\TypeError, RangeError tai ReferenceError saapuvat JS-ympäristöön vastaavana JS-virhetyyppinä, ja mikä tahansa muu Phasis\Exceptions\RuntimeError saapuu tavallisena Error-virheenä, molemmat omalla viestilläsi.

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

Monipuolisten arvojen palauttaminen

Paluuarvo muunnetaan sääntöjen PHP→JS mukaisesti. Jos haluat palauttaa JS-olioliteraalin, palauta PHP-assosiatiivinen taulukko:

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

Jos haluat palauttaa JS-taulukon, palauta peräkkäisistä alkioista koostuva PHP-lista:

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-olion, mukaan lukien ReactPHP-, Amp- tai Guzzle-ympäristöstä palautetun PHP-promisen, palauttaminen tuottaa arvon JS undefined (katso Arvojen muunnos).

Asynkroniset isäntäfunktiot

Isäntäfunktiot ovat synkronisia. PHP-promise- tai muuta thenable-oliota ei odoteta: se muunnetaan muotoon undefined, joten await hostFn() ratkeaa muotoon undefined. Suorita työ synkronisesti ja palauta tulos; JS-koodi voi silti await kutsua, jolloin arvo kääritään ratkaistuun promiseen:

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

Erityisesti kohteessa HTTP sisäänrakennettu fetch() palauttaa jo aidon JS-promisen, ja sen voi ohjata oman PHP-asiakkaan kautta mukautetulla fetch-kuljetuksella.

Suorituskykyhuomioita

Jokainen isäntäfunktiokutsu ylittää rajan PHP↔JS kerran argumenteille ja kerran paluuarvolle. Molemmat muunnokset kopioivat, joten niiden aikavaativuus on O(size of value) — primitiivit vievät vakioajan, taulukot ja oliot koon mukaan.

Tiukoissa sisäsilmukoissa kannattaa suosia yhtä enemmän työtä tekevää kutsua monen pienen kutsun sijaan ja välttää suurten taulukoiden edestakaista välittämistä, jos vain muutama kenttä muuttuu.