Lewati ke konten

Fungsi host

Setiap closure atau callable PHP yang diberikan kepada mesin melalui setGlobal() menjadi fungsi callable JS. Tidak perlu pendaftaran manual, skema, atau marshalling — cukup teruskan callable tersebut.

Dasar-dasar

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

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

Saat greet("world") dijalankan di JS:

  1. String "world" dikonversi dari JS ke PHP.
  2. Closure PHP dipanggil dengan argumen yang telah dikonversi.
  3. Nilai yang dikembalikan dikonversi dari PHP kembali ke JS.

Konversi mengikuti aturan konversi nilai. Argumen disalin: array atau objek JS diterima sebagai array PHP, dan perubahan padanya di PHP tidak mengubah nilai JS.

Arity dan parameter

Mesin meneruskan argumen JS ke callable sesuai posisinya, tanpa perubahan; mesin tidak memeriksa tanda tangan fungsi. Closure biasa mengabaikan argumen tambahan, sedangkan parameter variadik mengumpulkannya. Argumen yang tidak ada tidak diisi: jika JS meneruskan argumen lebih sedikit daripada yang diperlukan callable, PHP melempar ArgumentCountError. Berikan nilai bawaan pada parameter opsional.

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

Konversi tipe

Argumen dikonversi ke PHP menggunakan aturan JS→PHP. Engine memanggil callable Anda dari kode yang mendeklarasikan strict_types=1, sehingga PHP memeriksa tipe parameter skalar secara ketat dan tidak melakukan koersi: angka JS yang diberikan ke parameter string adalah TypeError. Seperti exception PHP lainnya dari fungsi host, exception ini diteruskan ke pemanggil eval(), bukan ke JS (lihat di bawah).

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

Lakukan koersi secara manual di dalam closure ((string) $value) atau gunakan tipe union (int|string).

Melempar kembali ke JS

Exception PHP biasa yang dilempar di dalam fungsi host saat ini tidak dikonversi menjadi error JS. Exception tersebut melewati setiap handler JS try / catch dan .catch(), lalu mencapai kode PHP yang memanggil eval() atau call(), dengan kelas aslinya (masalah #20). Ini mencakup \Exception, \RuntimeException, \DivisionByZeroError, serta error \TypeError dan \ArgumentCountError PHP yang muncul akibat argumen yang tidak valid.

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
}

Untuk memunculkan error yang dapat ditangkap oleh kode JS, lempar salah satu kelas error bawaan engine: Phasis\Exceptions\TypeError, RangeError atau ReferenceError muncul di JS sebagai tipe error JS yang sesuai, sedangkan Phasis\Exceptions\RuntimeError lainnya muncul sebagai Error biasa, masing-masing dengan pesan Anda.

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

Mengembalikan nilai kompleks

Nilai yang dikembalikan dikonversi dengan aturan PHP→JS. Untuk mengembalikan literal objek JS, kembalikan array asosiatif 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);
');

Untuk mengembalikan array JS, kembalikan daftar PHP berurutan:

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

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

Mengembalikan objek PHP, termasuk promise PHP dari ReactPHP, Amp atau Guzzle, menghasilkan JS undefined (lihat Konversi nilai).

Fungsi host asinkron

Fungsi host bersifat sinkron. Promise PHP atau objek thenable lainnya tidak ditunggu: objek tersebut dikonversi menjadi undefined, sehingga await hostFn() menghasilkan undefined. Lakukan pekerjaan secara sinkron dan kembalikan hasilnya; kode JS tetap dapat await pemanggilan tersebut, yang membungkus nilainya dalam promise yang telah diselesaikan:

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

Khusus untuk HTTP, fetch() bawaan sudah mengembalikan promise JS sungguhan dan dapat diarahkan melalui klien PHP Anda sendiri dengan transport fetch khusus.

Catatan performa

Setiap pemanggilan fungsi host melintasi batas PHP↔JS sekali untuk argumen dan sekali untuk nilai kembalian. Kedua konversi menyalin data, sehingga biayanya O(size of value) — primitif memerlukan waktu konstan, sedangkan array dan objek memerlukan waktu sebanding dengan ukurannya.

Untuk loop dalam yang sering dijalankan, pilih satu pemanggilan yang melakukan lebih banyak pekerjaan daripada banyak pemanggilan kecil, dan hindari bolak-balik mengirim array besar jika hanya beberapa field yang berubah.