Fonctions hôtes
Toute fermeture ou fonction appelable PHP transmise au moteur via setGlobal() devient une fonction JS appelable. Aucune étape d’enregistrement manuel, aucun schéma ni marshalling : transmettez simplement la fonction.
Bases
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Lorsque greet("world") s’exécute dans JS :
- La chaîne
"world"est convertie de JS en PHP. - La fermeture PHP est appelée avec l’argument converti.
- La valeur de retour est reconvertie de PHP en JS.
La conversion suit les règles de conversion des valeurs. Les arguments sont des copies : un tableau ou objet JS devient un tableau PHP, et le modifier dans PHP ne change pas la valeur JS.
Arity et paramètres
Le moteur transmet à la fonction appelable les arguments JS dans l’ordre, tels quels ; il n’inspecte pas la signature. Les arguments supplémentaires sont ignorés par les fermetures ordinaires et regroupés par les paramètres variadiques. Les arguments manquants ne sont pas ajoutés : si JS transmet moins d’arguments que la fonction n’en exige, PHP lance ArgumentCountError. Attribuez des valeurs par défaut aux paramètres facultatifs.
$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
Conversion implicite des types
Les arguments sont convertis en PHP selon les règles JS→PHP. Le moteur appelle votre fonction depuis du code qui déclare strict_types=1 ; PHP vérifie donc strictement les types des paramètres scalaires et n’effectue aucune coercition : un nombre JS transmis à un paramètre string provoque une erreur TypeError. Comme toute exception PHP provenant d’une fonction hôte, elle remonte jusqu’à l’appelant de eval(), et non à JS (voir ci-dessous).
$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
Vous pouvez effectuer une conversion manuelle dans la fermeture ((string) $value) ou utiliser des types union (int|string).
Propager une exception vers JS
À ce jour, les exceptions PHP ordinaires lancées dans une fonction hôte ne sont pas converties en erreurs JS. Elles traversent chaque gestionnaire JS try / catch et .catch(), puis atteignent le code PHP qui a appelé eval() ou call(), avec leur classe d’origine (problème #20). Cela inclut \Exception, \RuntimeException, \DivisionByZeroError ainsi que les exceptions PHP \TypeError et \ArgumentCountError générées en cas d’arguments incorrects.
$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
}
Pour lancer une erreur que le code JS peut intercepter, lancez l’une des classes d’erreur du moteur : Phasis\Exceptions\TypeError, RangeError ou ReferenceError devient dans JS le type d’erreur JS correspondant ; toute autre Phasis\Exceptions\RuntimeError devient une simple Error, avec votre message dans les deux cas.
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
}
');
Renvoyer des valeurs complexes
La valeur de retour est convertie selon les règles PHP→JS. Pour renvoyer un objet littéral JS, renvoyez un tableau associatif 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);
');
Pour renvoyer un tableau JS, renvoyez une liste séquentielle PHP :
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
Renvoyer un objet PHP, y compris une promesse PHP issue de ReactPHP, Amp ou Guzzle, donne JS undefined (voir Conversion des valeurs).
Fonctions hôtes asynchrones
Les fonctions hôtes sont synchrones. Une promesse PHP ou un autre objet thenable n’est pas attendu : il est converti en undefined, donc await hostFn() renvoie undefined. Effectuez le travail de façon synchrone et renvoyez le résultat ; le code JS peut tout de même await l’appel, ce qui encapsule la valeur dans une promesse déjà résolue :
$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();
');
Pour HTTP en particulier, le fetch() intégré renvoie déjà une véritable promesse JS et peut utiliser votre propre client PHP via un transport fetch personnalisé.
Notes sur les performances
Chaque appel de fonction hôte franchit une fois la frontière PHP↔JS pour les arguments et une fois pour la valeur de retour. Les deux conversions effectuent une copie : leur coût est donc O(size of value) — constant pour les primitives et proportionnel à la taille des tableaux et des objets.
Dans les boucles internes très sollicitées, préférez un seul appel qui effectue davantage de travail à de nombreux petits appels, et évitez de faire passer de grands tableaux dans les deux sens si seuls quelques champs changent.