
# Værtsfunktioner

Enhver PHP-closure eller callable, der gives til motoren via `setGlobal()`, bliver til en callable JS-funktion. Der kræves ingen manuel registrering, intet skema og ingen marshalling — giv blot funktionen videre.

## Grundlæggende

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

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

Når `greet("world")` kører i JS:

1. Strengen `"world"` konverteres fra JS til PHP.
2. PHP-closen kaldes med det konverterede argument.
3. Returværdien konverteres fra PHP tilbage til JS.

Konverteringen følger [reglerne for værdikonvertering](https://phasis.inline0.dev/da/docs/interop/values). Argumenterne er kopier: et JS-array eller -objekt modtages som et PHP-array, og ændringer i PHP ændrer ikke værdien i JS.

## Argumentantal og parametre

Motoren sender JS-argumenterne til den kaldbare funktion positionsvis, som de er; den undersøger ikke signaturen. Almindelige closures ignorerer ekstra argumenter, mens variadiske parametre samler dem. Manglende argumenter udfyldes ikke: hvis JS sender færre argumenter, end funktionen kræver, kaster PHP `ArgumentCountError`. Giv valgfrie parametre standardværdier.

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

## Typekonvertering

Argumenter konverteres til PHP efter reglerne for JS→PHP. Motoren kalder din callable fra kode, der erklærer `strict_types=1`, så PHP kontrollerer skalære parametertyper strengt og foretager ingen tvangskonvertering: et JS tal, der gives til en `string`-parameter, er en `TypeError`. Som enhver PHP-undtagelse fra en værtsfunktion når den kaldende kode i `eval()` i stedet for JS (se nedenfor).

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

Foretag selv tvangskonvertering i closure-funktionen (`(string) $value`), eller brug union-typer (`int|string`).

## Kast tilbage til JS

Almindelige PHP-undtagelser, der kastes i en værtsfunktion, konverteres i øjeblikket **ikke** til JS-fejl. De går gennem alle JS `try` / `catch`- og `.catch()`-håndteringer og når den PHP kode, der kaldte `eval()` eller `call()`, med deres oprindelige klasse ([issue #20](https://github.com/inline0/phasis/issues/20)). Det gælder `\Exception`, `\RuntimeException`, `\DivisionByZeroError` og de `\TypeError` og `\ArgumentCountError` PHP, der opstår ved ugyldige argumenter.

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

Hvis du vil rejse en fejl, som JS-kode kan fange, skal du kaste en af motorens egne fejklasser: `Phasis\Exceptions\TypeError`, `RangeError` eller `ReferenceError` ankommer i JS som den tilsvarende JS-fejltype, og enhver anden `Phasis\Exceptions\RuntimeError` ankommer som en almindelig `Error`, begge med din meddelelse.

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

## Returnering af komplekse værdier

Returværdien konverteres efter reglerne for PHP→JS. Hvis du vil returnere et JS-objektliteral, skal du returnere et PHP-associativt array:

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

Hvis du vil returnere et JS-array, skal du returnere en sekventiel PHP-liste:

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

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

Returnering af et PHP-objekt, herunder et PHP-promise fra ReactPHP, Amp eller Guzzle, giver JS `undefined` (se [Værdikonvertering](https://phasis.inline0.dev/da/docs/interop/values)).

## Asynkrone værtsfunktioner

Værtsfunktioner er synkrone. Et PHP-promise eller et andet thenable-objekt afventes ikke: det konverteres til `undefined`, så `await hostFn()` opløses til `undefined`. Udfør arbejdet synkront, og returner resultatet; JS-kode kan stadig `await` kaldet, som indpakker værdien i et allerede løst promise:

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

For specifikt HTTP returnerer den indbyggede `fetch()` allerede et ægte JS-promise, som kan sendes gennem din egen PHP-klient med en [brugerdefineret fetch-transport](https://phasis.inline0.dev/da/docs/interop/fetch-transport).

## Ydelsesnoter

Hvert kald af en værtsfunktion krydser grænsen mellem PHP og JS én gang for argumenterne og én gang for returværdien. Begge konverteringer kopierer, så de er O(size of value) — primitive værdier tager konstant tid, mens arrays og objekter koster proportionalt med deres størrelse.

I tætte indre løkker bør du foretrække ét kald, der udfører mere arbejde, frem for mange små kald, og undgå at sende store arrays frem og tilbage, når kun få felter ændres.
