Value conversion
Every call that crosses the PHP↔JS boundary — setGlobal, call, a host function invoked from JS — goes through one of two converters:
- PHP → JS when host values flow into the engine.
- JS → PHP when JS values flow back out.
The mappings are lossless for primitives and references for objects.
PHP → JS
| PHP value | JS result |
|---|---|
null | null |
true / false | true / false |
int | number (IEEE 754 double; ints beyond 2^53 lose precision) |
float | number (NaN, ±Infinity, ±0 preserved) |
string | string (UTF-8 PHP bytes interpreted as UTF-16 code units) |
list array (numeric, sequential keys from 0) | Array |
associative array (string keys) | plain Object |
Closure / callable | function |
object | object (passed by reference; see Shared objects) |
iterable / Generator | Array (eagerly materialised) |
| resource | Object with [[HostResource]] slot (opaque to JS) |
Mixed-key arrays (PHP's [0 => 'a', 'name' => 'b']) are treated as plain objects because Arrays in JS must have a contiguous integer index space.
$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 ObjectJS → PHP
| JS value | PHP result |
|---|---|
undefined | null |
null | null |
boolean | bool |
number (integral, within int range) | int |
number (other) | float |
bigint | string (decimal representation; PHP has no bigint type) |
string | string (UTF-8) |
symbol | string "Symbol(description)" — symbols aren't first-class in PHP |
Array | list array |
plain Object | associative array |
function | Closure wrapping the JS function |
Date | DateTimeImmutable (UTC) |
Map / Set | associative array / list (lossy — order preserved) |
Promise | resolved value (engine drives microtasks to settlement before returning) |
Error and subclasses | array with name and message keys (when used as a return value); thrown errors become JsThrowable |
TypedArray / ArrayBuffer | PHP string containing the raw bytes |
host-bound object | original PHP object (round-trip preserved) |
$arr = $engine->eval('[1, 2, 3]'); // [1, 2, 3]
$obj = $engine->eval('({a: 1, b: 2})'); // ['a' => 1, 'b' => 2]
$big = $engine->eval('1234567890123456789n'); // "1234567890123456789"
$d = $engine->eval('new Date(0)'); // DateTimeImmutable("1970-01-01T00:00:00+00:00")
$buf = $engine->eval('new Uint8Array([1,2,3])'); // "\x01\x02\x03"Numbers and precision
JavaScript numbers are IEEE 754 doubles. PHP ints are platform-native (64-bit on every modern build). The conversion preserves integers up to ±2^53 exactly; larger PHP ints become number and lose precision past 16 decimal digits. Use BigInt if you need exact arithmetic on integers larger than that.
NaN, ±Infinity, and ±0 round-trip exactly in both directions.
Strings
PHP strings are byte arrays; JS strings are UTF-16 code unit sequences. The engine bridges them by interpreting PHP bytes as UTF-8 and re-encoding into UTF-16 internally. This is lossless for valid UTF-8 input.
Invalid byte sequences are replaced with U+FFFD on the PHP→JS side and emitted as the platform's UTF-8 replacement on the JS→PHP side.
String.length, indexed access (s[0]), and slicing all count UTF-16 code units per the ECMAScript spec — not codepoints and not bytes. Strings containing astral characters like '🍕' therefore have .length === 2 (the two surrogate halves of U+1F355).
Round-trip stability
For most workloads, eval('JSON.parse(JSON.stringify(x))') and round-tripping a PHP value through setGlobal + call + return produce identical structures. The exceptions:
- bigints become decimal strings on the PHP side. Re-injecting them as
setGlobal('n', '123n')does not recreate the bigint — you'd need to evaluate the expressionBigInt('123')in JS. - Symbols lose their identity; converting back from PHP produces a fresh string.
- TypedArrays become opaque byte strings on the PHP side. To round-trip, pass the typed array as a host-bound object instead of letting it convert.
- Functions stay callable when round-tripping, but their JS prototype chain is hidden behind the wrapper.