
# Перетворення значень

Кожен виклик через межу 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` (число IEEE 754 double; цілі числа понад 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/uk/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 — послідовності 16-бітних кодових одиниць. Рушій з’єднує їх, трактуючи байти PHP як UTF-8 і повторно кодуючи їх у UTF-16. Для коректних вхідних даних UTF-8 перетворення не призводить до втрат. Байти, що не є коректним UTF-8, передаються без змін, а не замінюються на U+FFFD.

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

## Стабільність зворотного перетворення

Звичайні дані переживають перетворення туди й назад: значення PHP зі скалярів, списків і асоціативних масивів, передане через `setGlobal()` і повернуте через `eval()` або `call()`, зберігає ту саму структуру, окрім того, що цілі числа з рухомою комою повертаються як `int` (`1.0` повертає `1`).

Усе інше з наведених вище таблиць, що перетворюється на `null`, `undefined` або `[]`, не зберігається після зворотного перетворення.
