
# Функції хоста

Будь-яке замикання або функція PHP, передана рушію через `setGlobal()`, стає функцією JS, яку можна викликати. Не потрібні ручна реєстрація, схема чи маршалізація — просто передайте функцію.

## Основи

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

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

Коли `greet("world")` виконується в JS:

1. Рядок `"world"` перетворюється з JS на PHP.
2. Викликається замикання PHP із перетвореним аргументом.
3. Повернене значення перетворюється з PHP назад на JS.

Перетворення відбувається за [правилами перетворення значень](https://phasis.inline0.dev/uk/docs/interop/values). Аргументи копіюються: масив або об’єкт JS надходить як масив PHP, і його зміни в PHP не змінюють значення JS.

## Кількість аргументів і параметри

Рушій передає функції JS аргументи в заданому порядку без перевірки її сигнатури. Зайві аргументи звичайні замикання ігнорують, а варіативні параметри збирають. Відсутні аргументи не підставляються: якщо JS передає менше аргументів, ніж потрібно функції, PHP викидає `ArgumentCountError`. Для необов’язкових параметрів задавайте значення за замовчуванням.

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

## Приведення типів

Аргументи конвертуються в PHP за правилами JS→PHP. Рушій викликає вашу функцію з коду, у якому оголошено `strict_types=1`, тому PHP суворо перевіряє скалярні типи параметрів і не виконує приведення: число JS, передане параметру `string`, спричиняє `TypeError`. Як і будь-який виняток PHP із функції хост-середовища, він доходить до виклику `eval()`, а не до JS (див. нижче).

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

Або виконайте приведення вручну всередині замикання (`(string) $value`), або використайте об’єднані типи (`int|string`).

## Передавання винятків назад у JS

Звичайні винятки PHP, викинуті всередині функції хост-середовища, наразі **не** перетворюються на помилки JS. Вони проходять крізь усі обробники JS `try` / `catch` і `.catch()` та доходять до коду PHP, який викликав `eval()` або `call()`, з початковим класом ([помилка №20](https://github.com/inline0/phasis/issues/20)). Це стосується `\Exception`, `\RuntimeException`, `\DivisionByZeroError`, а також помилок `\TypeError` і `\ArgumentCountError` PHP, що виникають через некоректні аргументи.

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

Щоб викинути помилку, яку може перехопити код JS, викиньте один із власних класів помилок рушія: `Phasis\Exceptions\TypeError`, `RangeError` або `ReferenceError` надходять у JS як відповідний тип помилки JS, а будь-який інший `Phasis\Exceptions\RuntimeError` надходить як звичайний `Error`; у кожному разі з вашим повідомленням.

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

## Повернення складних значень

Повернене значення конвертується за правилами PHP→JS. Щоб повернути літерал об’єкта JS, поверніть асоціативний масив 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);
');
```

Щоб повернути масив JS, поверніть послідовний список 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
```

Повернення об’єкта PHP, зокрема промісу PHP із ReactPHP, Amp або Guzzle, спричиняє JS `undefined` (див. [Перетворення значень](https://phasis.inline0.dev/uk/docs/interop/values)).

## Асинхронні функції хост-середовища

Функції хост-середовища синхронні. Проміс PHP або інший thenable-об’єкт не очікується: він конвертується в `undefined`, тож `await hostFn()` повертає `undefined`. Виконайте роботу синхронно й поверніть результат; код JS усе одно може `await` виклик, обгорнувши значення у виконаний проміс:

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

Для HTTP вбудований `fetch()` уже повертає справжній проміс JS, тож його можна спрямувати через власний клієнт PHP із [власним транспортом fetch](https://phasis.inline0.dev/uk/docs/interop/fetch-transport).

## Примітки щодо продуктивності

Кожен виклик функції хост-середовища один раз перетинає межу PHP↔JS для аргументів і ще раз для поверненого значення. Обидва перетворення копіюють дані, тому їхня складність становить O(size of value): примітиви обробляються за сталий час, а вартість для масивів і об’єктів пропорційна їхньому розміру.

Для гарячих внутрішніх циклів краще виконати більше роботи за один виклик, ніж робити багато дрібних викликів, і не передавати великі масиви туди й назад, якщо змінюється лише кілька полів.
