
# Хост функции

Всяко затваряне или извикваем обект на 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/bg/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()`, със запазен оригинален клас ([issue #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/bg/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/bg/docs/interop/fetch-transport).

## Бележки за производителността

Всяко извикване на хост функция преминава границата PHP↔JS веднъж за аргументите и веднъж за връщаната стойност. И двете преобразувания копират, затова сложността им е O(size of value) — примитивите са с константно време, а масивите и обектите струват пропорционално на размера си.

При често изпълнявани вътрешни цикли предпочитайте едно извикване, което върши повече работа, пред много малки извиквания, и избягвайте да прехвърляте големи масиви напред-назад, когато се променят само няколко полета.
