Skip to content

Value conversion

Every call that crosses the PHP↔JS boundary — setGlobal, call, a host function invoked from JS — goes through a converter:

  • PHP → JS when host values flow into the engine (setGlobal(), call() arguments, host-function return values).
  • JS → PHP when JS values flow back out. There are two paths with slightly different rules: results of eval() and call(), and the arguments JS passes to a host function.

Conversion copies. Primitives map to primitives, arrays and objects are copied into new structures, and PHP callables are wrapped as JS functions. Nothing is shared by reference except the callables themselves.

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), even when it names a PHP function
list array (keys 0, 1, 2, … in order) new Array, elements converted recursively
any other array new plain Object, values converted recursively
Closure, invokable object, callable array such as [$object, 'method'] function that calls the PHP callable
any other object, Generator, resource undefined

Mixed-key arrays (PHP's [0 => 'a', 'name' => 'b']) become plain objects because Arrays in JS must have a contiguous integer index space.

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

Because arrays are copied, JS changes stay in JS:

PHP
$list = [1, 2, 3];
$engine->setGlobal('list', $list);
$engine->eval('list.push(4)');
count($list);                 // 3
$engine->eval('list.length'); // 4

PHP objects that are not callable do not convert; they arrive as undefined (issue #18). Convert them to arrays first, for example with get_object_vars(), or expose their methods as closures. See Shared objects.

JS → PHP: eval() and call() results

JS value PHP result
undefined, null null
boolean bool
number (integral, within int range) int (-0 becomes 0)
NaN, Infinity, -Infinity NAN, INF, -INF
number (other) float
string string (UTF-8)
bigint, symbol null
function null
Array list array, elements converted recursively
TypedArray list array of numbers
any other object (plain objects, Error, Date, Map, Set, Promise, ArrayBuffer, …) associative array of its own data properties, skipping accessors and function-valued properties

The last row means objects whose state lives in internal slots convert to an empty array: new Date(0), new Map([["a", 1]]) and Promise.resolve(5) all return []. An Error returns its own message and stack. A self-referencing object converts its inner references to 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)');              // []

Convert inside JS when you need one of those values in PHP:

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: host-function arguments

Arguments JS passes to a PHP host function follow the same rules for primitives, arrays and plain objects, with these differences:

JS value PHP argument
TypedArray, ArrayBuffer, DataView the engine's own JsValue object, unconverted
object associative array of all its own properties, including accessors (whose getters run) and function-valued properties
arrow function an array (usually empty)
function declared with the function keyword, or any object that references itself no conversion: the copy never terminates and PHP runs out of memory (issue #21)

There is no way yet to receive a JS callback in a host function. Pass data, not functions, across this boundary.

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 inside JS if you need exact arithmetic on integers larger than that, and pass the result out as a string.

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. Bytes that are not valid UTF-8 are passed through unchanged rather than replaced with U+FFFD.

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

Plain data survives a round trip: a PHP value made of scalars, lists and associative arrays that goes in through setGlobal() and comes back through eval() or call() produces the same structure, except that integral floats come back as int (1.0 returns 1).

Everything else in the tables above that maps to null, undefined or [] does not round-trip.