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
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
When greet("world") runs in JS:
- The string
"world"is converted from JS to PHP. - The PHP closure is invoked with the converted argument.
- The return value is converted from PHP back to JS.
Conversion follows the value conversion rules. 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.
$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).
$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). That includes \Exception, \RuntimeException, \DivisionByZeroError, and the \TypeError and \ArgumentCountError PHP raises for bad arguments.
$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.
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:
$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:
$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).
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:
$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.
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.