Pāriet uz saturu

Resursdatora funkcijas

Jebkura PHP slēgtā funkcija vai izsaucams objekts, kas nodots dzinējam, izmantojot setGlobal(), kļūst par izsaucamu JS funkciju. Manuāla reģistrācija nav vajadzīga, nav shēmas vai maršalizācijas — vienkārši nododiet izsaucamo objektu.

Pamati

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

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

Kad greet("world") darbojas JS:

  1. Virkne "world" tiek pārveidota no JS uz PHP.
  2. Tiek izsaukta PHP slēgtā funkcija ar pārveidoto argumentu.
  3. Atgrieztā vērtība tiek pārveidota no PHP atpakaļ uz JS.

Pārveide notiek saskaņā ar vērtību pārveides noteikumiem. Argumenti ir kopijas: JS masīvs vai objekts tiek saņemts kā PHP masīvs, un tā izmainīšana PHP nemaina JS vērtību.

Argumentu skaits un parametri

Dzinējs nodod JS argumentus izsaucamajam objektam norādītajā secībā un tieši tādus, kādi tie ir; tas nepārbauda signatūru. Parastās slēgtās funkcijas ignorē liekos argumentus, bet mainīga argumentu skaita parametri tos apkopo. Trūkstošie argumenti netiek aizpildīti: ja JS nodod mazāk argumentu, nekā nepieciešams izsaucamajam objektam, PHP izmet ArgumentCountError. Izvēles parametriem norādiet noklusējuma vērtības.

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

Tipu automātiska pārveidošana

Argumenti tiek pārveidoti par PHP, izmantojot JS→PHP noteikumus. Dzinējs izsauc jūsu izsaucamo objektu no koda, kas deklarē strict_types=1, tāpēc PHP stingri pārbauda skalāro parametru tipus un neveic automātisku pārveidi: JS skaitlis, kas nodots string parametram, ir TypeError. Tāpat kā jebkurš PHP izņēmums no resursdatora funkcijas, tas nonāk pie eval() izsaucēja, nevis pie JS (skatiet tālāk).

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

Veiciet manuālu pārveidi slēgtajā funkcijā ((string) $value) vai izmantojiet apvienotos tipus (int|string).

Kļūdu izmešana atpakaļ uz JS

Parasti PHP izņēmumi, kas izmesti resursdatora funkcijā, pašlaik netiek pārveidoti par JS kļūdām. Tie iziet cauri visiem JS try / catch un .catch() apstrādātājiem un nonāk līdz PHP kodam, kas izsauca eval() vai call(), saglabājot sākotnējo klasi (20. problēma). Tas attiecas arī uz \Exception, \RuntimeException, \DivisionByZeroError un \TypeError un \ArgumentCountError PHP, kas tiek izmests nederīgu argumentu dēļ.

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
}

Lai izmestu kļūdu, ko var uztvert JS kods, izmetiet kādas no dzinēja kļūdu klasēm eksemplāru: Phasis\Exceptions\TypeError, RangeError vai ReferenceError tiek saņemti JS kā atbilstošā JS kļūdas tipa eksemplāri, bet jebkura cita Phasis\Exceptions\RuntimeError tiek saņemta kā parasts Error; katrā gadījumā tiek saglabāts jūsu ziņojums.

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

Bagātīgu vērtību atgriešana

Atgrieztā vērtība tiek pārveidota saskaņā ar PHP→JS noteikumiem. Lai atgrieztu JS objekta literāli, atgrieziet PHP asociatīvo masīvu:

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

Lai atgrieztu JS masīvu, atgrieziet secīgu PHP sarakstu:

PHP
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));

$engine->eval('range(1, 5).reduce((a, b) => a + b)');  // 15

Atgriežot PHP objektu, tostarp PHP solījumu no ReactPHP, Amp vai Guzzle, tiek iegūts JS undefined (skatiet Vērtību pārveide).

Asinhronās resursdatora funkcijas

Resursdatora funkcijas ir sinhronas. PHP solījums vai cits thenable objekts netiek gaidīts: tas tiek pārveidots par undefined, tāpēc await hostFn() tiek atrisināts par undefined. Veiciet darbu sinhroni un atgrieziet rezultātu; JS kods joprojām var izsaukt šo darbību ar await, kas iesaiņo vērtību atrisinātā solījumā:

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

Tieši HTTP gadījumā iebūvētais fetch() jau atgriež īstu JS solījumu, un to var novirzīt caur jūsu PHP klientu ar pielāgotu izgūšanas transportu.

Veiktspējas piezīmes

Katrs resursdatora funkcijas izsaukums šķērso PHP↔JS robežu vienreiz argumentiem un vienreiz atgrieztajai vērtībai. Abas pārveides veido kopijas, tāpēc to sarežģītība ir O(size of value) — primitīviem tipiem laiks ir konstants, bet masīviem un objektiem tas pieaug proporcionāli to izmēram.

Veiktspējai svarīgās iekšējās cilpās dodiet priekšroku vienam izsaukumam, kas paveic vairāk, nevis daudziem maziem izsaukumiem, un nepārsūtiet lielus masīvus turp un atpakaļ, ja mainās tikai daži lauki.