
# Host functions

Any PHP closure or callable handed to the engine via `setGlobal()` becomes a callable JS function. There's no manual registration step, no schema, no marshalling — just pass the callable.

## Basics

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

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

When `greet("world")` runs in JS:

1. The string `"world"` is converted from JS to PHP.
2. The PHP closure is invoked with the converted argument.
3. The return value is converted from PHP back to JS.

Conversion follows the [value conversion rules](/docs/interop/values). Arguments are copies: a JS array or object arrives as a PHP array, and changing it in PHP does not change the JS value.

## Arity and parameters

The engine passes the JS arguments to the callable positionally, as they are; it does not inspect the signature. Extra arguments are ignored by ordinary closures and collected by variadic parameters. Missing arguments are not filled in: if JS passes fewer arguments than the callable requires, PHP throws `ArgumentCountError`. Give optional parameters default values.

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

## Type juggling

Arguments are converted to PHP using the JS→PHP rules. The engine calls your callable from code that declares `strict_types=1`, so PHP checks scalar parameter types strictly and does not coerce: a JS number passed to a `string` parameter is a `TypeError`. Like any PHP exception from a host function, it reaches the caller of `eval()` rather than JS (see below).

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

Either coerce manually inside the closure (`(string) $value`) or use union types (`int|string`).

## Throwing back into JS

Ordinary PHP exceptions thrown inside a host function are **not** converted into JS errors today. They pass through every JS `try` / `catch` and `.catch()` handler and reach the PHP code that called `eval()` or `call()`, with their original class ([issue #20](https://github.com/inline0/phasis/issues/20)). That includes `\Exception`, `\RuntimeException`, `\DivisionByZeroError`, and the `\TypeError` and `\ArgumentCountError` PHP raises for bad arguments.

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

To raise an error that JS code can catch, throw one of the engine's own error classes: `Phasis\Exceptions\TypeError`, `RangeError` or `ReferenceError` arrive in JS as the matching JS error type, and any other `Phasis\Exceptions\RuntimeError` arrives as a plain `Error`, each with your message.

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

## Returning rich values

The return value is converted with the PHP→JS rules. To return a JS object literal, return a PHP associative array:

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

To return a JS array, return a sequential PHP list:

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

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

Returning a PHP object, including a PHP promise from ReactPHP, Amp or Guzzle, gives JS `undefined` (see [Value conversion](/docs/interop/values)).

## Async host functions

Host functions are synchronous. A PHP promise or other thenable object is not awaited: it converts to `undefined`, so `await hostFn()` resolves to `undefined`. Do the work synchronously and return the result; JS code can still `await` the call, which wraps the value in a resolved promise:

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

For HTTP specifically, the built-in `fetch()` already returns a real JS promise and can be routed through your own PHP client with a [custom fetch transport](/docs/interop/fetch-transport).

## Performance notes

Each host-function call crosses the PHP↔JS boundary once for arguments and once for the return value. Both conversions copy, so they are O(size of value) — primitives are constant-time, arrays and objects cost proportionally to their size.

For hot inner loops, prefer one call that does more work over many small calls, and avoid passing large arrays back and forth when only a few fields change.
