
# Host-Funktionen

Jeder PHP-Closure oder jedes aufrufbare Objekt, das der Engine über `setGlobal()` übergeben wird, wird zu einer aufrufbaren JS-Funktion. Es ist keine manuelle Registrierung, kein Schema und kein Marshalling nötig – übergib einfach das Aufrufbare.

## Grundlagen

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

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

Wenn `greet("world")` in JS ausgeführt wird:

1. Die Zeichenfolge `"world"` wird von JS in PHP umgewandelt.
2. Der PHP-Closure wird mit dem konvertierten Argument aufgerufen.
3. Der Rückgabewert wird von PHP zurück in JS umgewandelt.

Die Konvertierung folgt den [Wertkonvertierungsregeln](https://phasis.inline0.dev/de/docs/interop/values). Argumente sind Kopien: Ein JS-Array oder -Objekt kommt als PHP-Array an, und Änderungen daran in PHP ändern den JS-Wert nicht.

## Anzahl und Parameter

Die Engine übergibt die JS-Argumente positionsweise und unverändert an das Aufrufbare; die Signatur wird nicht geprüft. Gewöhnliche Closures ignorieren zusätzliche Argumente, variadische Parameter sammeln sie. Fehlende Argumente werden nicht ergänzt: Übergibt JS weniger Argumente, als das Aufrufbare erfordert, löst PHP einen Fehler mit `ArgumentCountError` aus. Gib optionalen Parametern Standardwerte.

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

## Typumwandlung

Argumente werden nach den JS→PHP-Regeln in PHP umgewandelt. Die Engine ruft dein Aufrufbares aus Code auf, der `strict_types=1` deklariert. Daher prüft PHP skalare Parametertypen strikt und nimmt keine Umwandlung vor: Eine JS-Zahl als Argument für einen `string`-Parameter ist ein `TypeError`. Wie jede PHP-Ausnahme aus einer Host-Funktion erreicht sie den Aufrufer von `eval()` statt JS (siehe unten).

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

Wandle den Wert manuell innerhalb des Closures um (`(string) $value`) oder verwende Union-Typen (`int|string`).

## Fehler an JS weitergeben

Gewöhnliche PHP-Ausnahmen, die innerhalb einer Host-Funktion ausgelöst werden, werden derzeit **nicht** in JS-Fehler umgewandelt. Sie durchlaufen alle JS-`try`-/`catch`- und `.catch()`-Handler und erreichen den PHP-Code, der `eval()` oder `call()` aufgerufen hat, mit ihrer ursprünglichen Klasse ([Issue #20](https://github.com/inline0/phasis/issues/20)). Dazu gehören `\Exception`, `\RuntimeException`, `\DivisionByZeroError` sowie die von PHP ausgelösten `\TypeError`- und `\ArgumentCountError`-Fehler bei ungültigen Argumenten.

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

Damit JS-Code einen Fehler abfangen kann, löse eine der eigenen Fehlerklassen der Engine aus: `Phasis\Exceptions\TypeError`, `RangeError` oder `ReferenceError` kommen in JS als entsprechender JS-Fehlertyp an; jeder andere `Phasis\Exceptions\RuntimeError` kommt als einfacher `Error` an, jeweils mit deiner Meldung.

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

## Komplexe Werte zurückgeben

Der Rückgabewert wird nach den PHP→JS-Regeln umgewandelt. Um ein JS-Objektliteral zurückzugeben, gib ein assoziatives PHP-Array zurück:

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

Um ein JS-Array zurückzugeben, gib eine sequenzielle PHP-Liste zurück:

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

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

Bei Rückgabe eines PHP-Objekts – einschließlich eines PHP-Promises von ReactPHP, Amp oder Guzzle – erhält JS `undefined` (siehe [Wertkonvertierung](https://phasis.inline0.dev/de/docs/interop/values)).

## Asynchrone Host-Funktionen

Host-Funktionen sind synchron. Ein PHP-Promise oder anderes Thenable-Objekt wird nicht abgewartet: Es wird in `undefined` umgewandelt, sodass `await hostFn()` zu `undefined` aufgelöst wird. Erledige die Arbeit synchron und gib das Ergebnis zurück. JS-Code kann den Aufruf weiterhin mit `await` ausführen, wodurch der Wert in ein aufgelöstes Promise verpackt wird:

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

Speziell für HTTP gibt die integrierte Funktion `fetch()` bereits ein echtes JS-Promise zurück und kann über einen [benutzerdefinierten Fetch-Transport](https://phasis.inline0.dev/de/docs/interop/fetch-transport) an deinen eigenen PHP-Client weitergeleitet werden.

## Hinweise zur Leistung

Jeder Aufruf einer Host-Funktion überquert die PHP↔JS-Grenze einmal für die Argumente und einmal für den Rückgabewert. Beide Konvertierungen erstellen Kopien und sind daher O(size of value) – primitive Werte haben konstante Laufzeit, Arrays und Objekte kosten proportional zu ihrer Größe.

In häufig ausgeführten inneren Schleifen solltest du einen Aufruf bevorzugen, der mehr Arbeit erledigt, statt viele kleine Aufrufe zu machen. Übergib außerdem keine großen Arrays hin und her, wenn sich nur wenige Felder ändern.
