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

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

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