Phasis
Interop

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 valueJS result
nullnull
true / falsetrue / false
intnumber (IEEE 754 double; ints beyond 2^53 lose precision)
floatnumber (NaN, ±Infinity, ±0 preserved)
stringstring (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 / callablefunction
objectobject (passed by reference; see Shared objects)
iterable / GeneratorArray (eagerly materialised)
resourceObject 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 Object

JS → PHP

JS valuePHP result
undefinednull
nullnull
booleanbool
number (integral, within int range)int
number (other)float
bigintstring (decimal representation; PHP has no bigint type)
stringstring (UTF-8)
symbolstring "Symbol(description)" — symbols aren't first-class in PHP
Arraylist array
plain Objectassociative array
functionClosure wrapping the JS function
DateDateTimeImmutable (UTC)
Map / Setassociative array / list (lossy — order preserved)
Promiseresolved value (engine drives microtasks to settlement before returning)
Error and subclassesarray with name and message keys (when used as a return value); thrown errors become JsThrowable
TypedArray / ArrayBufferPHP string containing the raw bytes
host-bound objectoriginal 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 expression BigInt('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.

On this page