Funciones del host
Cualquier cierre o función invocable PHP que se pase al motor mediante setGlobal() se convierte en una función invocable JS. No hace falta registrarla manualmente, definir un esquema ni serializarla: basta con pasar la función.
Conceptos básicos
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Cuando greet("world") se ejecuta en JS:
- La cadena
"world"se convierte de JS a PHP. - Se invoca el cierre PHP con el argumento convertido.
- El valor devuelto se convierte de PHP a JS.
La conversión sigue las reglas de conversión de valores. Los argumentos son copias: un array o un objeto JS llega como un array PHP nuevo, y modificarlo en PHP no cambia el valor de JS.
Aridad y parámetros
El motor pasa los argumentos de JS a la función invocable por posición y tal como están; no inspecciona la firma. Los cierres ordinarios ignoran los argumentos adicionales y los parámetros variádicos los recopilan. No se completan los argumentos que falten: si JS pasa menos argumentos de los que exige la función invocable, PHP lanza ArgumentCountError. Asigna valores predeterminados a los parámetros opcionales.
$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
Conversión de tipos
Los argumentos se convierten a PHP según las reglas de JS→PHP. El motor llama a tu función invocable desde código que declara strict_types=1, por lo que PHP comprueba estrictamente los tipos de los parámetros escalares y no convierte los valores: pasar un número JS a un parámetro string es un TypeError. Como cualquier excepción PHP de una función del host, llega a quien llama a eval(), no a JS (véase más abajo).
$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
Convierte el valor manualmente dentro del cierre ((string) $value) o usa tipos unión (int|string).
Propagar excepciones a JS
Las excepciones ordinarias de PHP lanzadas dentro de una función del host no se convierten en errores JS actualmente. Atraviesan cada manejador try / catch y .catch() de JS, y llegan al código PHP que llamó a eval() o call(), conservando su clase original (incidencia n.º 20). Esto incluye \Exception, \RuntimeException, \DivisionByZeroError y las excepciones que PHP \TypeError y \ArgumentCountError lanzan para argumentos incorrectos.
$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
}
Para lanzar un error que pueda capturar el código JS, lanza una de las clases de error propias del motor: Phasis\Exceptions\TypeError, RangeError o ReferenceError llegan a JS como el tipo de error JS correspondiente; cualquier otro Phasis\Exceptions\RuntimeError llega como un Error simple, en ambos casos con tu mensaje.
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
}
');
Devolver valores complejos
El valor devuelto se convierte según las reglas de PHP→JS. Para devolver un objeto literal JS, devuelve un array asociativo 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);
');
Para devolver un array JS, devuelve una lista secuencial PHP:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
Devolver un objeto PHP, incluida una promesa PHP de ReactPHP, Amp o Guzzle, produce JS undefined (consulta Conversión de valores).
Funciones asíncronas del host
Las funciones del host son síncronas. Una promesa PHP u otro objeto thenable no se espera: se convierte en undefined, así que await hostFn() se resuelve como undefined. Realiza el trabajo de forma síncrona y devuelve el resultado; el código JS puede aplicar await a la llamada, que envuelve el valor en una promesa resuelta:
$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();
');
Para HTTP en concreto, fetch() integrado ya devuelve una promesa JS real y puede dirigirse a tu cliente PHP mediante un transporte fetch personalizado.
Notas de rendimiento
Cada llamada a una función del host cruza una vez el límite PHP↔JS para los argumentos y otra para el valor devuelto. Ambas conversiones hacen copias, por lo que su coste es O(size of value): constante para valores primitivos y proporcional al tamaño para arrays y objetos.
En bucles internos muy frecuentes, es mejor hacer más trabajo en una sola llamada que muchas llamadas pequeñas. Evita pasar arrays grandes de un lado a otro cuando solo cambien unos pocos campos.