Wertkonvertierung
Jeder Aufruf über die PHP↔JS-Grenze – setGlobal, call oder eine aus JS aufgerufene Host-Funktion – durchläuft einen Konverter:
- PHP → JS, wenn Werte aus dem Host in die Engine gelangen (
setGlobal(),call()-Argumente, Rückgabewerte von Host-Funktionen). - JS → PHP, wenn JS-Werte zurückgegeben werden. Dafür gibt es zwei Pfade mit leicht unterschiedlichen Regeln: Ergebnisse von
eval()undcall()sowie Argumente, die JS an eine Host-Funktion übergibt.
Bei der Konvertierung werden Kopien erstellt. Primitive Werte werden auf primitive Werte abgebildet, Arrays und Objekte in neue Strukturen kopiert und PHP-Aufrufbare als JS-Funktionen verpackt. Abgesehen von den Aufrufbaren wird nichts per Referenz gemeinsam genutzt.
PHP → JS
| PHP-Wert | JS-Ergebnis |
|---|---|
null |
null |
true / false |
true / false |
int |
number (IEEE 754 Double; Ganzzahlen über 2^53 verlieren an Genauigkeit) |
float |
number (NaN, ±Infinity, ±0 bleiben erhalten) |
string |
string (UTF-8-PHP-Bytes werden als UTF-16-Codeeinheiten interpretiert), auch wenn sie eine PHP-Funktion benennen |
Listen-array (Schlüssel 0, 1, 2, … der Reihe nach) |
neues Array, Elemente rekursiv konvertiert |
jedes andere array |
neues einfaches Object, Werte rekursiv konvertiert |
Closure, aufrufbares Objekt, aufrufbares Array wie [$object, 'method'] |
function, das das PHP-Aufrufbare aufruft |
jedes andere Objekt, Generator, Ressource |
undefined |
Arrays mit gemischten Schlüsseln (PHPs [0 => 'a', 'name' => 'b']) werden zu einfachen Objekten, da Arrays in JS einen lückenlosen Bereich ganzzahliger Indizes benötigen.
$engine->setGlobal('list', [1, 2, 3]); // → JS Array
$engine->setGlobal('map', ['a' => 1, 'b' => 2]); // → JS Object
$engine->setGlobal('mixed', [0 => 'x', 'k' => 'v']); // → JS Object
Da Arrays kopiert werden, bleiben Änderungen an JS in JS:
$list = [1, 2, 3];
$engine->setGlobal('list', $list);
$engine->eval('list.push(4)');
count($list); // 3
$engine->eval('list.length'); // 4
Nicht aufrufbare PHP-Objekte lassen sich nicht konvertieren; sie kommen als undefined an (Issue #18). Wandle sie zuerst in Arrays um, zum Beispiel mit get_object_vars(), oder stelle ihre Methoden als Closures bereit. Siehe Gemeinsam genutzte Objekte.
JS → PHP: Ergebnisse von eval() und call()
| JS-Wert | PHP-Ergebnis |
|---|---|
undefined, null |
null |
boolean |
bool |
number (ganzzahlig, innerhalb des int-Bereichs) |
int (-0 wird zu 0) |
NaN, Infinity, -Infinity |
NAN, INF, -INF |
number (sonstige) |
float |
string |
string (UTF-8) |
bigint, symbol |
null |
function |
null |
Array |
Liste array, Elemente rekursiv konvertiert |
TypedArray |
Liste array mit Zahlen |
jedes andere Objekt (einfache Objekte, Error, Date, Map, Set, Promise, ArrayBuffer, …) |
assoziatives array seiner eigenen Dateneigenschaften, wobei Accessors und Eigenschaften mit Funktionswert übersprungen werden |
Die letzte Zeile bedeutet, dass Objekte mit internem Zustand zu einem leeren Array werden: new Date(0), new Map([["a", 1]]) und Promise.resolve(5) geben alle [] zurück. Ein Error gibt sein eigenes message und stack zurück. Bei einem Objekt mit Selbstreferenz werden die internen Referenzen zu null.
$engine->eval('[1, 2, 3]'); // [1, 2, 3]
$engine->eval('({a: 1, b: 2})'); // ['a' => 1, 'b' => 2]
$engine->eval('new Uint8Array([1, 2, 3])'); // [1, 2, 3]
$engine->eval('1234567890123456789n'); // null
$engine->eval('new Date(0)'); // []
Wandle die betreffenden Werte in JS um, wenn du sie in PHP brauchst:
$engine->eval('(1234567890123456789n).toString()'); // "1234567890123456789"
$engine->eval('new Date(0).toISOString()'); // "1970-01-01T00:00:00.000Z"
$engine->eval('Object.fromEntries(new Map([["a", 1]]))'); // ['a' => 1]
JS → PHP: Argumente von Host-Funktionen
Argumente, die JS an eine PHP-Host-Funktion übergibt, folgen bei primitiven Werten, Arrays und einfachen Objekten denselben Regeln, mit folgenden Unterschieden:
| JS-Wert | PHP-Argument |
|---|---|
TypedArray, ArrayBuffer, DataView |
das eigene JsValue-Objekt der Engine, unverändert |
| Objekt | assoziatives array aller eigenen Eigenschaften, einschließlich Accessors (deren Getter ausgeführt werden) und Eigenschaften mit Funktionswert |
| Arrow-Funktion | ein array (meist leer) |
function mit dem Schlüsselwort function deklariert oder ein beliebiges Objekt mit Selbstreferenz |
keine Konvertierung: Die Kopie endet nie und PHP hat keinen Speicher mehr (Issue #21) |
Eine JS-Callback-Funktion lässt sich derzeit nicht in einer Host-Funktion empfangen. Übergib Daten, keine Funktionen, über diese Grenze.
Zahlen und Genauigkeit
JavaScript-Zahlen sind IEEE-754-Double-Werte. PHP-Ganzzahlen sind plattformnativ (bei allen modernen Builds 64-Bit). Die Konvertierung erhält Ganzzahlen bis ±2^53 exakt; größere PHP-Ganzzahlen werden zu number und verlieren ab 16 Dezimalstellen an Genauigkeit. Verwende BigInt in JS, wenn du größere Ganzzahlen exakt berechnen musst, und gib das Ergebnis als Zeichenfolge aus.
Zeichenfolgen
PHP-Zeichenfolgen sind Byte-Arrays; JS-Zeichenfolgen sind Folgen von UTF-16-Codeeinheiten. Die Engine überbrückt den Unterschied, indem sie PHP-Bytes als UTF-8 interpretiert und intern erneut als UTF-16 kodiert. Bei gültiger UTF-8-Eingabe bleibt alles unverändert. Ungültige UTF-8-Bytes werden unverändert durchgereicht, statt durch U+FFFD ersetzt zu werden.
String.length, der indizierte Zugriff (s[0]) und das Slicing zählen gemäß der ECMAScript-Spezifikation UTF-16-Codeeinheiten – keine Codepoints und keine Bytes. Strings mit astralen Zeichen wie '🍕' haben daher .length === 2 (die beiden Surrogathälften von U+1F355).
Stabilität beim Roundtrip
Einfache Daten überstehen einen Roundtrip: Ein PHP-Wert aus Skalaren, Listen und assoziativen Arrays, der über setGlobal() eingegeben und über eval() oder call() zurückgegeben wird, hat dieselbe Struktur – mit Ausnahme ganzzahliger Floats, die als int zurückkommen (1.0 gibt 1 zurück).
Alles andere in den obigen Tabellen, das auf null, undefined oder [] abgebildet wird, ist nicht roundtripfähig.