본문으로 건너뛰기

값 변환

setGlobal, call, JS에서 호출되는 호스트 함수 등 PHP↔JS 경계를 통과하는 모든 호출은 변환기를 거칩니다:

  • 호스트 값이 엔진으로 들어올 때의 PHP → JS (setGlobal(), call() 인수, 호스트 함수 반환값).
  • JS 값이 다시 나갈 때의 JS → PHP. 규칙이 약간 다른 두 가지 경로가 있습니다. eval() 및 call()의 결과와 JS가 호스트 함수에 전달하는 인수입니다.

변환은 복사 방식입니다. 원시 값은 원시 값으로 매핑되고, 배열과 객체는 새 구조로 복사되며, PHP 호출 가능 객체는 JS 함수로 래핑됩니다. 호출 가능 객체 자체를 제외하면 참조로 공유되는 것은 없습니다.

PHP → JS

PHP 값 JS 결과
null null
true / false true / false
int number (IEEE 754 double; 2^53를 넘는 정수는 정밀도를 잃음)
float number (NaN, ±Infinity, ±0 보존)
string string (UTF-8 PHP 바이트를 UTF-16 코드 단위로 해석), PHP 함수 이름인 경우에도 해당
리스트 array (키 0, 1, 2, … 순서대로) 새 Array, 요소를 재귀적으로 변환
그 밖의 모든 array 새 일반 Object, 값을 재귀적으로 변환
Closure, 호출 가능한 객체, [$object, 'method']와 같은 호출 가능한 배열 function, PHP 호출 가능 객체를 호출
그 밖의 모든 객체, Generator, 리소스 undefined

혼합 키 배열(PHP의 [0 => 'a', 'name' => 'b'])은 일반 객체가 됩니다. JS의 배열은 연속된 정수 인덱스 공간을 가져야 하기 때문입니다.

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

배열은 복사되므로 JS에서 변경해도 JS에 그대로 남습니다:

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

호출할 수 없는 PHP 객체는 변환되지 않고 undefined로 전달됩니다(이슈 #18). 먼저 get_object_vars() 등을 사용해 배열로 변환하거나 메서드를 클로저로 노출하세요. 공유 객체를 참조하세요.

JS → PHP: eval() 및 call() 결과

JS 값 PHP 결과
undefined, null null
boolean bool
number (정수이며 int 범위 내) int (-0는 0이 됨)
NaN, Infinity, -Infinity NAN, INF, -INF
number (기타) float
string string (UTF-8)
bigint, symbol null
function null
Array 리스트 array, 요소를 재귀적으로 변환
TypedArray 숫자로 이루어진 리스트 array
그 밖의 모든 객체(일반 객체, Error, Date, Map, Set, Promise, ArrayBuffer, …) 자체 데이터 속성을 담은 연관 array. 접근자와 함수 값 속성은 제외

마지막 행은 내부 슬롯에 상태를 저장하는 객체가 빈 배열로 변환된다는 뜻입니다. new Date(0), new Map([["a", 1]]), Promise.resolve(5)는 모두 []을 반환합니다. Error는 자체 message 및 stack을 반환합니다. 자기 자신을 참조하는 객체는 내부 참조를 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)');              // []

이러한 값 중 하나가 PHP에서 필요하면 JS 내부에서 변환하세요:

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: 호스트 함수 인수

JS가 PHP 호스트 함수에 전달하는 인수는 원시 값, 배열, 일반 객체에 대해 같은 규칙을 따르며 다음과 같은 차이가 있습니다:

JS 값 PHP 인수
TypedArray, ArrayBuffer, DataView 변환되지 않은 엔진 자체의 JsValue 객체
객체 접근자(게터 실행)와 함수 값 속성을 포함한 모든 자체 속성의 연관 array
화살표 함수 array (대개 비어 있음)
function 키워드로 선언된 function 또는 자기 자신을 참조하는 객체 변환하지 않음: 복사가 끝나지 않아 PHP 메모리가 부족해집니다(이슈 #21)

아직 호스트 함수에서 JS 콜백을 받을 방법은 없습니다. 이 경계를 넘어갈 때는 함수가 아닌 데이터를 전달하세요.

숫자와 정밀도

JavaScript 숫자는 IEEE 754 배정밀도입니다. PHP 정수는 플랫폼 고유 형식입니다(모든 최신 빌드에서 64비트). 변환은 ±2^53까지의 정수를 정확하게 보존합니다. 더 큰 PHP 정수는 number이 되며 소수점 이하 16자리 이후의 정밀도를 잃습니다. 그보다 큰 정수도 정확히 계산해야 한다면 JS 내부에서 BigInt를 사용하고 결과를 문자열로 전달하세요.

문자열

PHP 문자열은 바이트 배열이고 JS 문자열은 UTF-16 코드 단위 시퀀스입니다. 엔진은 PHP 바이트를 UTF-8로 해석한 뒤 내부에서 UTF-16으로 다시 인코딩하여 두 형식 사이를 변환합니다. 유효한 UTF-8 입력은 손실 없이 변환됩니다. 유효한 UTF-8이 아닌 바이트는 U+FFFD로 대체되지 않고 그대로 전달됩니다.

String.length, 인덱스 접근(s[0]), 슬라이싱은 모두 ECMAScript 사양에 따라 바이트나 코드 포인트가 아닌 UTF-16 코드 단위를 셉니다. 따라서 '🍕'와 같은 보충 평면 문자가 포함된 문자열은 .length === 2을 갖습니다(U+1F355의 서로게이트 상위 및 하위 쌍).

왕복 변환 안정성

일반 데이터는 왕복 변환 후에도 유지됩니다. 스칼라, 리스트, 연관 배열로 이루어진 PHP 값이 setGlobal()을 통해 들어간 뒤 eval() 또는 call()을 통해 돌아오면 동일한 구조가 됩니다. 단, 정수형 실수는 int로 돌아오며(1.0는 1을 반환) 예외입니다.

위 표에서 null, undefined 또는 []로 매핑되는 나머지 값은 왕복 변환되지 않습니다.