
# Преобразование значений

Каждый вызов, пересекающий границу PHP↔JS — `setGlobal`, `call` или вызов хост-функции из JS — проходит через преобразователь:

- **PHP → JS**, когда значения хост-среды передаются в движок: `setGlobal()`, аргументы `call()`, возвращаемые хост-функциями значения.
- **JS → PHP**, когда значения JS передаются обратно. Есть два пути с немного различающимися правилами: результаты `eval()` и `call()`, а также аргументы, которые JS передаёт хост-функции.

При преобразовании выполняется копирование. Примитивы преобразуются в примитивы, массивы и объекты копируются в новые структуры, а вызываемые объекты PHP оборачиваются в функции JS. По ссылке разделяются только сами вызываемые объекты.

## PHP → JS

| Значение PHP | Результат JS |
|---|---|
| `null` | `null` |
| `true` / `false` | `true` / `false` |
| `int` | `number` (double IEEE 754; целые числа за пределами 2^53 теряют точность) |
| `float` | `number` (NaN, ±Infinity, ±0 сохраняются) |
| `string` | `string` (байты PHP UTF-8 интерпретируются как кодовые единицы 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](https://github.com/inline0/phasis/issues/18)). Сначала преобразуйте их в массивы, например с помощью `get_object_vars()`, или предоставьте их методы через замыкания. См. раздел [Общие объекты](https://phasis.inline0.dev/ru/docs/interop/shared-objects).

## 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)');              // []
```

Преобразуйте значение внутри JS, если оно нужно вам в 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: аргументы хост-функций

Аргументы, которые JS передаёт хост-функции PHP, преобразуются по тем же правилам для примитивов, массивов и обычных объектов, за исключением следующего:

| Значение JS | Аргумент PHP |
|---|---|
| `TypedArray`, `ArrayBuffer`, `DataView` | собственный объект `JsValue` движка, без преобразования |
| объект | ассоциативный `array` со всеми собственными свойствами, включая аксессоры (их геттеры вызываются) и свойства-функции |
| стрелочная функция | `array` (обычно пустой) |
| `function`, объявленная с ключевым словом `function`, или любой объект со ссылкой на себя | не преобразуется: копирование никогда не завершается, и PHP заканчивается память ([проблема №21](https://github.com/inline0/phasis/issues/21)) |

Пока нет способа получить обратный вызов JS в хост-функции. Передавайте через эту границу данные, а не функции.

## Числа и точность

Числа JavaScript — это числа с плавающей запятой двойной точности IEEE 754. Целые числа PHP имеют платформенный формат (во всех современных сборках — 64-разрядный). При преобразовании целые числа до ±2^53 сохраняются точно; более крупные целые числа PHP становятся `number` и теряют точность после 16 десятичных цифр. Для точных вычислений с более крупными целыми числами используйте `BigInt` внутри JS, а результат передавайте в виде строки.

## Строки

Строки PHP — это массивы байтов; строки JS — последовательности кодовых единиц UTF-16. Движок соединяет их, интерпретируя байты PHP как UTF-8 и перекодируя их во внутреннее представление UTF-16. Для корректных входных данных UTF-8 преобразование выполняется без потерь. Байты, не являющиеся корректным UTF-8, передаются без изменений, а не заменяются на U+FFFD.

`String.length`, индексированный доступ (`s[0]`) и извлечение среза считают кодовые единицы UTF-16 согласно спецификации ECMAScript, а не кодовые точки и не байты. Поэтому строки с символами за пределами базовой многоязычной плоскости, например `'🍕'`, имеют `.length === 2` — две суррогатные половины U+1F355.

## Стабильность при обратном преобразовании

Обычные данные сохраняются при обратном преобразовании: значение PHP, состоящее из скаляров, списков и ассоциативных массивов, которое передаётся через `setGlobal()` и возвращается через `eval()` или `call()`, сохраняет исходную структуру. Исключение: целые числа с плавающей точкой возвращаются как `int` (`1.0` возвращает `1`).

Все остальные значения из таблиц выше, которые преобразуются в `null`, `undefined` или `[]`, не сохраняются при обратном преобразовании.
