
# 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](https://phasis.inline0.dev/id/docs/interop/values). 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](https://github.com/inline0/phasis/issues/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](https://phasis.inline0.dev/id/docs/interop/values)).

## 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](https://phasis.inline0.dev/id/docs/interop/fetch-transport).

## 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.
