
# Преобразуване на стойности

Всяко извикване, което преминава границата 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; цели числа над 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` ([issue #18](https://github.com/inline0/phasis/issues/18)). Първо ги преобразувайте в масиви, например с `get_object_vars()`, или предоставете методите им като затваряния. Вижте [Споделени обекти](https://phasis.inline0.dev/bg/docs/interop/shared-objects).

## JS → PHP: резултати `eval()` и `call()`

| Стойност JS | Резултат PHP |
|---|---|
| `undefined`, `null` | `null` |
| `boolean` | `bool` |
| `number` (цяло число в допустимия диапазон) | `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 свършва паметта ([issue #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` или `[]`, не запазва стойността си при обратно преобразуване.
