
# Функции хоста

Любое замыкание или вызываемый объект 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/ru/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/ru/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/ru/docs/interop/fetch-transport).

## Примечания о производительности

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

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