
# Gostiteljske funkcije

Vsaka zaprta funkcija ali klicni objekt PHP, ki ga posredujete mehanizmu prek `setGlobal()`, postane klicna funkcija JS. Ročna registracija, shema ali posredovanje podatkov ni potrebno — preprosto posredujte klicno funkcijo.

## Osnove

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

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

Ko se `greet("world")` izvede v JS:

1. Niz `"world"` se pretvori iz JS v PHP.
2. Zaprta funkcija PHP se pokliče s pretvorjenim argumentom.
3. Vrnjena vrednost se pretvori iz PHP nazaj v JS.

Pretvorba sledi [pravilom pretvorbe vrednosti](https://phasis.inline0.dev/sl/docs/interop/values). Argumenti so kopije: tabela ali objekt JS prispe kot tabela PHP, zato spremembe v PHP ne spremenijo vrednosti JS.

## Število argumentov in parametri

Mehanizem posreduje JS argumente klicni funkciji po vrsti, takšne, kot so; podpisa ne preverja. Običajne zaprte funkcije prezrejo odvečne argumente, variadični parametri pa jih zberejo. Manjkajoči argumenti niso dodani: če JS posreduje manj argumentov, kot jih klicna funkcija zahteva, PHP sproži `ArgumentCountError`. Izbirnim parametrom določite privzete vrednosti.

```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
```

## Prilagajanje tipov

Argumenti se pretvorijo v PHP po pravilih JS→PHP. Mehanizem pokliče vašo funkcijo iz kode, ki deklarira `strict_types=1`, zato PHP strogo preverja vrste skalarnih parametrov in jih ne pretvarja: število JS, posredovano parametru `string`, povzroči `TypeError`. Tako kot vsaka izjema PHP iz gostiteljske funkcije tudi ta doseže klicatelja `eval()`, namesto da bi dosegla JS (glejte spodaj).

```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
```

V zaprti funkciji lahko tip pretvorite ročno (`(string) $value`) ali uporabite unijske tipe (`int|string`).

## Sprožanje napak v JS

Običajne izjeme PHP, sprožene v gostiteljski funkciji, se trenutno **ne** pretvorijo v napake JS. Potujejo skozi vse upravljalnike JS `try`/`catch` in `.catch()` ter dosežejo kodo PHP, ki je poklicala `eval()` ali `call()`, z ohranjenim izvirnim razredom ([težava št. 20](https://github.com/inline0/phasis/issues/20)). To vključuje `\Exception`, `\RuntimeException`, `\DivisionByZeroError` ter napaki `\TypeError` in `\ArgumentCountError` PHP, ki se sprožita ob neveljavnih argumentih.

```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
}
```

Če želite sprožiti napako, ki jo lahko ujame koda JS, sprožite enega od lastnih razredov napak mehanizma: `Phasis\Exceptions\TypeError`, `RangeError` ali `ReferenceError` se v JS pojavijo kot ustrezna vrsta napake JS, vse druge napake `Phasis\Exceptions\RuntimeError` pa kot navadne napake `Error`, vsaka z vašim sporočilom.

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

## Vračanje kompleksnih vrednosti

Vrnjena vrednost se pretvori po pravilih PHP→JS. Če želite vrniti objektni literal JS, vrnite asociativno tabelo 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);
');
```

Če želite vrniti tabelo JS, vrnite zaporedni seznam 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
```

Če vrnete objekt PHP, vključno z obljubo PHP iz ReactPHP, Amp ali Guzzle, dobite JS `undefined` (glejte [Pretvorba vrednosti](https://phasis.inline0.dev/sl/docs/interop/values)).

## Asinhrone gostiteljske funkcije

Gostiteljske funkcije so sinhrone. Objava PHP ali drug objekt thenable se ne čaka; pretvori se v `undefined`, zato se `await hostFn()` razreši v `undefined`. Delo opravite sinhrono in vrnite rezultat; koda JS lahko še vedno uporabi `await` za klic, ki vrednost ovije v razrešeno obljubo:

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

Za HTTP vgrajeni `fetch()` že vrne pravo obljubo JS, ki jo lahko usmerite prek lastnega odjemalca PHP s [prenosom fetch po meri](https://phasis.inline0.dev/sl/docs/interop/fetch-transport).

## Opombe o zmogljivosti

Vsak klic gostiteljske funkcije enkrat prečka mejo PHP↔JS za argumente in enkrat za vrnjeno vrednost. Obe pretvorbi ustvarita kopijo, zato je njun strošek O(size of value) — primitivi zahtevajo konstanten čas, tabele in objekti pa čas, sorazmeren njihovi velikosti.

V vročih notranjih zankah raje opravite več dela z enim klicem kot z mnogimi majhnimi in ne prenašajte velikih tabel sem ter tja, kadar se spremeni le nekaj polj.
