Hostfuncties
Elke PHP-closure of callable die via setGlobal() aan de engine wordt doorgegeven, wordt een aanroepbare JS-functie. Je hoeft niets handmatig te registreren, geen schema op te geven of gegevens te marshallen — geef de callable gewoon door.
Basis
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Wanneer greet("world") in JS wordt uitgevoerd:
- De tekenreeks
"world"wordt omgezet van JS naar PHP. - De PHP-closure wordt aangeroepen met het omgezette argument.
- De retourwaarde wordt terug omgezet van PHP naar JS.
De conversie volgt de regels voor waardeconversie. Argumenten zijn kopieën: een JS-array of -object komt aan als PHP-array. Wijzigingen eraan in PHP veranderen de JS-waarde niet.
Aantal en parameters
De engine geeft de JS-argumenten positioneel en ongewijzigd door aan de callable; de functiehandtekening wordt niet gecontroleerd. Gewone closures negeren extra argumenten, terwijl variadische parameters ze verzamelen. Ontbrekende argumenten worden niet aangevuld: als JS minder argumenten doorgeeft dan de callable vereist, gooit PHP ArgumentCountError. Geef optionele parameters standaardwaarden.
$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
Typeconversie
Argumenten worden volgens de regels JS→PHP omgezet naar PHP. De engine roept je callable aan vanuit code die strict_types=1 declareert, dus PHP controleert scalare parametertypen strikt en voert geen typeconversie uit: een JS getal dat aan een parameter van het type string wordt doorgegeven, is een TypeError. Net als elke PHP-uitzondering van een hostfunctie bereikt deze de aanroeper van eval(), in plaats van JS (zie hieronder).
$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
Voer de typeconversie handmatig uit binnen de closure ((string) $value) of gebruik uniontypen (int|string).
Teruggooien naar JS
Gewone uitzonderingen PHP die binnen een hostfunctie worden gegenereerd, worden momenteel niet omgezet in JS-fouten. Ze gaan door elke JS try / catch- en .catch()-handler en bereiken de PHP-code die eval() of call() heeft aangeroepen, met hun oorspronkelijke klasse (probleem #20). Dat geldt ook voor \Exception, \RuntimeException, \DivisionByZeroError en de \TypeError- en \ArgumentCountError-PHP-fouten bij ongeldige argumenten.
$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
}
Als je een fout wilt gooien die JS-code kan opvangen, gooi dan een van de eigen foutklassen van de engine: Phasis\Exceptions\TypeError, RangeError of ReferenceError komen in JS aan als het overeenkomstige fouttype van JS, en elke andere Phasis\Exceptions\RuntimeError komt aan als een gewone Error, telkens met jouw bericht.
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
}
');
Rijke waarden retourneren
De retourwaarde wordt omgezet volgens de regels PHP→JS. Als je een objectliteral van het type JS wilt retourneren, geef dan een associatieve array van PHP terug:
$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);
');
Als je een array van JS wilt retourneren, geef dan een opeenvolgende lijst van PHP terug:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
Een object van PHP retourneren, waaronder een promise van ReactPHP, Amp of Guzzle van PHP, levert JS undefined op (zie Waardeconversie).
Asynchrone hostfuncties
Hostfuncties zijn synchroon. Er wordt niet gewacht op een promise van PHP of een ander thenable-object: dit wordt omgezet naar undefined, waardoor await hostFn() wordt opgelost naar undefined. Voer het werk synchroon uit en retourneer het resultaat; JS-code kan de aanroep nog steeds await, waardoor de waarde in een opgeloste promise wordt verpakt:
$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();
');
Voor HTTP specifiek retourneert de ingebouwde fetch() al een echte promise van JS en kan deze via je eigen PHP-client worden gerouteerd met een aangepast fetch-transport.
Opmerkingen over prestaties
Elke aanroep van een hostfunctie passeert de grens tussen PHP en JS eenmaal voor de argumenten en eenmaal voor de retourwaarde. Beide conversies maken een kopie, dus ze zijn O(size of value) — primitieven nemen constante tijd in beslag; arrays en objecten kosten tijd evenredig aan hun omvang.
Geef in hete binnenste lussen de voorkeur aan één aanroep die meer werk doet boven veel kleine aanroepen, en vermijd het heen en weer sturen van grote arrays wanneer slechts enkele velden veranderen.