
# Funkcje hosta

Każde domknięcie lub funkcja wywoływalna PHP przekazana silnikowi przez `setGlobal()` staje się funkcją JS. Nie trzeba jej ręcznie rejestrować, tworzyć schematu ani marshallować — wystarczy przekazać funkcję.

## Podstawy

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

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

Gdy `greet("world")` działa w JS:

1. Ciąg `"world"` jest konwertowany z JS na PHP.
2. Domknięcie PHP jest wywoływane z przekonwertowanym argumentem.
3. Wartość zwracana jest konwertowana z PHP z powrotem na JS.

Konwersja odbywa się zgodnie z [zasadami konwersji wartości](https://phasis.inline0.dev/pl/docs/interop/values). Argumenty są kopiami: tablica lub obiekt JS trafia jako tablica PHP, a zmiana wprowadzona w PHP nie zmienia wartości JS.

## Liczba argumentów i parametry

Silnik przekazuje funkcji wywoływalnej argumenty JS po kolei, bez zmian; nie analizuje jej sygnatury. Zwykłe domknięcia ignorują nadmiarowe argumenty, a parametry wariadyczne je zbierają. Brakujące argumenty nie są uzupełniane: jeśli JS przekaże ich mniej, niż wymaga funkcja, PHP zgłosi `ArgumentCountError`. Ustaw wartości domyślne dla parametrów opcjonalnych.

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

## Rzutowanie typów

Argumenty są konwertowane na PHP zgodnie z regułami JS→PHP. Silnik wywołuje przekazaną funkcję z kodu deklarującego `strict_types=1`, dlatego PHP ściśle sprawdza typy parametrów skalarnych i nie dokonuje konwersji: liczba JS przekazana do parametru `string` jest wartością `TypeError`. Podobnie jak każdy wyjątek PHP z funkcji hosta, trafia on do kodu wywołującego `eval()`, a nie JS (szczegóły poniżej).

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

Możesz ręcznie konwertować wartość wewnątrz domknięcia (`(string) $value`) albo użyć typów sumy (`int|string`).

## Zgłaszanie wyjątku z powrotem do JS

Zwykłe wyjątki PHP zgłaszane wewnątrz funkcji hosta **nie** są obecnie konwertowane na błędy JS. Przechodzą przez wszystkie procedury obsługi JS `try` / `catch` i `.catch()`, po czym trafiają do kodu PHP, który wywołał `eval()` lub `call()`, zachowując pierwotną klasę ([zgłoszenie #20](https://github.com/inline0/phasis/issues/20)). Dotyczy to również `\Exception`, `\RuntimeException`, `\DivisionByZeroError` oraz wyjątków `\TypeError` i `\ArgumentCountError` PHP zgłaszanych przy nieprawidłowych argumentach.

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

Aby zgłosić błąd przechwytywany przez kod JS, rzuć jedną z własnych klas błędów silnika: `Phasis\Exceptions\TypeError`, `RangeError` lub `ReferenceError` trafia do JS jako odpowiadający im typ błędu JS, a każdy inny `Phasis\Exceptions\RuntimeError` trafia jako zwykły `Error`; oba zawierają podany komunikat.

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

## Zwracanie złożonych wartości

Wartość zwracana jest konwertowana zgodnie z regułami PHP→JS. Aby zwrócić literał obiektu JS, zwróć tablicę asocjacyjną 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);
');
```

Aby zwrócić tablicę JS, zwróć sekwencyjną listę 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
```

Zwrócenie obiektu PHP, w tym obietnicy PHP z ReactPHP, Amp lub Guzzle, daje wartość JS `undefined` (zob. [konwersja wartości](https://phasis.inline0.dev/pl/docs/interop/values)).

## Asynchroniczne funkcje hosta

Funkcje hosta działają synchronicznie. Obietnica PHP lub inny obiekt thenable nie jest oczekiwany: zostaje przekonwertowany na `undefined`, więc `await hostFn()` zwraca `undefined`. Wykonuj pracę synchronicznie i zwracaj wynik; kod JS nadal może wywołać `await`, co opakowuje wartość w rozwiązaną obietnicę:

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

W przypadku samego HTTP wbudowana funkcja `fetch()` już zwraca prawdziwą obietnicę JS i można skierować ją do własnego klienta PHP za pomocą [własnego transportu fetch](https://phasis.inline0.dev/pl/docs/interop/fetch-transport).

## Uwagi dotyczące wydajności

Każde wywołanie funkcji hosta przekracza granicę PHP↔JS raz dla argumentów i raz dla wartości zwracanej. Obie konwersje kopiują dane, więc ich koszt jest O(size of value) — operacje na typach prostych mają stały czas, a koszt obsługi tablic i obiektów rośnie proporcjonalnie do ich rozmiaru.

W gorących pętlach wewnętrznych lepiej wykonać więcej pracy w jednym wywołaniu niż wiele małych wywołań. Unikaj też przesyłania dużych tablic tam i z powrotem, gdy zmienia się tylko kilka pól.
