Host-Funktionen
Jeder PHP-Closure oder jedes aufrufbare Objekt, das der Engine über setGlobal() übergeben wird, wird zu einer aufrufbaren JS-Funktion. Es ist keine manuelle Registrierung, kein Schema und kein Marshalling nötig – übergib einfach das Aufrufbare.
Grundlagen
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Wenn greet("world") in JS ausgeführt wird:
- Die Zeichenfolge
"world"wird von JS in PHP umgewandelt. - Der PHP-Closure wird mit dem konvertierten Argument aufgerufen.
- Der Rückgabewert wird von PHP zurück in JS umgewandelt.
Die Konvertierung folgt den Wertkonvertierungsregeln. Argumente sind Kopien: Ein JS-Array oder -Objekt kommt als PHP-Array an, und Änderungen daran in PHP ändern den JS-Wert nicht.
Anzahl und Parameter
Die Engine übergibt die JS-Argumente positionsweise und unverändert an das Aufrufbare; die Signatur wird nicht geprüft. Gewöhnliche Closures ignorieren zusätzliche Argumente, variadische Parameter sammeln sie. Fehlende Argumente werden nicht ergänzt: Übergibt JS weniger Argumente, als das Aufrufbare erfordert, löst PHP einen Fehler mit ArgumentCountError aus. Gib optionalen Parametern Standardwerte.
$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
Typumwandlung
Argumente werden nach den JS→PHP-Regeln in PHP umgewandelt. Die Engine ruft dein Aufrufbares aus Code auf, der strict_types=1 deklariert. Daher prüft PHP skalare Parametertypen strikt und nimmt keine Umwandlung vor: Eine JS-Zahl als Argument für einen string-Parameter ist ein TypeError. Wie jede PHP-Ausnahme aus einer Host-Funktion erreicht sie den Aufrufer von eval() statt JS (siehe unten).
$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
Wandle den Wert manuell innerhalb des Closures um ((string) $value) oder verwende Union-Typen (int|string).
Fehler an JS weitergeben
Gewöhnliche PHP-Ausnahmen, die innerhalb einer Host-Funktion ausgelöst werden, werden derzeit nicht in JS-Fehler umgewandelt. Sie durchlaufen alle JS-try-/catch- und .catch()-Handler und erreichen den PHP-Code, der eval() oder call() aufgerufen hat, mit ihrer ursprünglichen Klasse (Issue #20). Dazu gehören \Exception, \RuntimeException, \DivisionByZeroError sowie die von PHP ausgelösten \TypeError- und \ArgumentCountError-Fehler bei ungültigen 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
}
Damit JS-Code einen Fehler abfangen kann, löse eine der eigenen Fehlerklassen der Engine aus: Phasis\Exceptions\TypeError, RangeError oder ReferenceError kommen in JS als entsprechender JS-Fehlertyp an; jeder andere Phasis\Exceptions\RuntimeError kommt als einfacher Error an, jeweils mit deiner Meldung.
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
}
');
Komplexe Werte zurückgeben
Der Rückgabewert wird nach den PHP→JS-Regeln umgewandelt. Um ein JS-Objektliteral zurückzugeben, gib ein assoziatives PHP-Array zurück:
$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);
');
Um ein JS-Array zurückzugeben, gib eine sequenzielle PHP-Liste zurück:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
Bei Rückgabe eines PHP-Objekts – einschließlich eines PHP-Promises von ReactPHP, Amp oder Guzzle – erhält JS undefined (siehe Wertkonvertierung).
Asynchrone Host-Funktionen
Host-Funktionen sind synchron. Ein PHP-Promise oder anderes Thenable-Objekt wird nicht abgewartet: Es wird in undefined umgewandelt, sodass await hostFn() zu undefined aufgelöst wird. Erledige die Arbeit synchron und gib das Ergebnis zurück. JS-Code kann den Aufruf weiterhin mit await ausführen, wodurch der Wert in ein aufgelöstes Promise verpackt wird:
$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();
');
Speziell für HTTP gibt die integrierte Funktion fetch() bereits ein echtes JS-Promise zurück und kann über einen benutzerdefinierten Fetch-Transport an deinen eigenen PHP-Client weitergeleitet werden.
Hinweise zur Leistung
Jeder Aufruf einer Host-Funktion überquert die PHP↔JS-Grenze einmal für die Argumente und einmal für den Rückgabewert. Beide Konvertierungen erstellen Kopien und sind daher O(size of value) – primitive Werte haben konstante Laufzeit, Arrays und Objekte kosten proportional zu ihrer Größe.
In häufig ausgeführten inneren Schleifen solltest du einen Aufruf bevorzugen, der mehr Arbeit erledigt, statt viele kleine Aufrufe zu machen. Übergib außerdem keine großen Arrays hin und her, wenn sich nur wenige Felder ändern.