Zum Inhalt springen

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() und call() 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.

PHP
$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:

PHP
$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.

PHP
$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:

PHP
$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.