
# API

Середовище виконання має один клас API: `Phasis\Engine`. Усе інше (`JsValue`, `JsFunction`, `JsObject`, …) є внутрішньою частиною рушія й доступне лише хост-функціям, що з’єднують PHP↔JS. Інша публічна поверхня — окремий парсер [API](https://phasis.inline0.dev/uk/docs/parser) (`Phasis\Parser\Parser` і класи `Phasis\Ast`), який розбирає вихідний код без виконання.

## Створення

```php
use Phasis\Engine;

$engine = new Engine();                 // lazy mode (default)
$engine = new Engine(eager: true);      // install every built-in up front
```

Нове середовище `Engine` надає повну стандартну бібліотеку: `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` і базові вбудовані засоби мови встановлюються одразу, щоб специфікація працювала (без `Function.prototype` неможливі класи, без `Promise` — async/await, без `RegExp` — літерали регулярних виразів). Решта — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` і сімейство array-buffer, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, сімейство weak-ref, `Temporal`, `ShadowRealm`, Web Platform Pack і Fetch Pack — реєструється як заповнювач-аксесор і створюється під час першого читання. Читання глобальної змінної прозоре: `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')` — усе працює, а після першого читання заповнювач замінюється звичайним дескриптором даних.

Передайте `eager: true`, якщо потрібна поведінка до лінивого завантаження: усі вбудовані засоби встановлюються під час створення, а повний набір дескрипторів доступний для інтроспекції. Тестове середовище відповідності (`bin/test262`, `bin/wpt`) створює рушії в eager-режимі, щоб test262/WPT бачили очікувані форми дескрипторів.

## оцінювання

```php
public function eval(string $source): mixed
```

Розбирає та виконує `$source` як класичний скрипт у глобальній області видимості рушія, а потім запускає цикл подій до його спорожнення. Повертає значення останнього обчисленого виразу, перетворене на PHP (або `null`, якщо в програмі немає завершального виразу). Правила описано в розділі [Перетворення значень](https://phasis.inline0.dev/uk/docs/interop/values).

```php
$result = $engine->eval('1 + 2');           // 3
$result = $engine->eval('JSON.stringify({a: 1})');  // '{"a":1}'
$result = $engine->eval('[1,2,3].map(x => x * 2)'); // [2, 4, 6] (PHP array)
```

Promise не розгортається: `eval('Promise.resolve(5)')` повертає порожній масив. Оскільки на момент повернення `eval()` цикл подій уже спорожнений, збережіть виконане значення у глобальній змінній і прочитайте його другим викликом `eval()`:

```php
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out');   // 9
```

Явний JS `throw`, який ніщо не перехоплює, стає `Phasis\Exceptions\JsThrowable` із повідомленням `"Name: message"`, а його властивість `$jsValue` містить передане значення `JsValue`. Помилки самого рушія, наприклад читання властивості `null` або невизначеної змінної, стають натомість `Phasis\Exceptions\TypeError`, `ReferenceError` або `RangeError`. Усі вони успадковують `Phasis\Exceptions\RuntimeError`; помилки розбору успадковують `Phasis\Exceptions\SyntaxError`. Див. [Винятки](#exceptions).

```php
try {
    $engine->eval('throw new Error("boom")');
} catch (\Phasis\Exceptions\JsThrowable $e) {
    echo $e->getMessage();
    // Error: boom
}
```

## evalAsModule

```php
public function evalAsModule(string $source, ?string $path = null): mixed
```

Обчислює `$source` як модуль ES: доступні `import` / `export`, конструкції верхнього рівня `await` і `import.meta`, а відносні специфікатори розв’язуються відносно `$path` (абсолютного шляху до файлу). Цикл подій працює як у `eval()`; метод завжди повертає `null`. Щоб прочитати результати, використовуйте глобальні змінні або буфер консолі.

```php
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
```

Завантажувач розв’язує лише відносні й абсолютні шляхи; див. [Відомі обмеження](https://phasis.inline0.dev/uk/docs/compatibility/limitations#module-loader).

## execFile

```php
public function execFile(string $path): mixed
```

Читає файл із диска й передає його вміст до `eval()`, тож він запускається як класичний скрипт незалежно від розширення; файл із `import` / `export` спричинить SyntaxError. Реальний шлях до файлу стає базою для динамічних специфікаторів `import()`. Для файлів модулів використовуйте `evalAsModule()`.

```php
$engine->execFile(__DIR__ . '/script.js');
```

Якщо файл не вдається прочитати, PHP виводить попередження, а `execFile()` генерує звичайний `\RuntimeException` («Не вдалося прочитати файл: …»).

## setGlobal

```php
public function setGlobal(string $name, mixed $value): void
```

Прив’язує глобальну змінну, доступну в усіх наступних обчисленнях JS. Значення PHP перетворюються на JS:

| Тип PHP | Тип JS |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (навіть якщо це назва функції, як-от `'strlen'`) |
| масив (список) | `Array` (копія) |
| масив (асоціативний) | `Object` (копія) |
| `Closure`, об’єкт, який можна викликати, `[$object, 'method']` | `function` |
| будь-який інший об’єкт, генератор або ресурс | `undefined` |

Масиви копіюються: зміни, які JS вносить у них, не відображаються в масиві PHP. Звичайні об’єкти PHP поки що не можна передавати ([проблема #18](https://github.com/inline0/phasis/issues/18)); обхідні шляхи описано в розділі [Спільні об’єкти](https://phasis.inline0.dev/uk/docs/interop/shared-objects).

```php
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
```

Усередині JS:

```javascript
console.log(config.version);   // "1.0"
log('hello');                  // calls the PHP closure
```

## виклик

```php
public function call(string $name, mixed ...$args): mixed
```

Викликає глобальну функцію JS з аргументами PHP (перетвореними так само, як у `setGlobal`). Повернене значення перетворюється назад на PHP. Виклик невизначеного імені спричиняє `Phasis\Exceptions\ReferenceError`; виклик глобальної змінної, яка не є функцією, — `Phasis\Exceptions\TypeError`.

```php
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);   // 5
```

## Виведення консолі

```php
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
```

`console.log`, `console.info`, `console.warn`, `console.error` і `console.debug` нічого не виводять. Кожен виклик додає до буфера рушія один відформатований рядок. `getConsoleLines()` повертає буферизовані рядки списком, `getConsoleOutput()` повертає їх, об’єднані через `\n`, а `clearConsole()` очищає буфер. Буфер не очищається між викликами `eval()`.

```php
$engine->eval('console.log("a", 1); console.warn({ b: 2 })');
$engine->getConsoleLines();    // ['a 1', '{ b: 2 }']
echo $engine->getConsoleOutput();
// a 1
// { b: 2 }
$engine->clearConsole();
```

## setLimit

```php
public function setLimit(string $limit, int $value): void
```

Встановлює змінюваний під час виконання ліміт ресурсу. Такий ліміт лише один:

| Ліміт | Типове значення | Опис |
|---|---|---|
| `maxLoopIterations` | `100000` | максимальна кількість ітерацій за один запуск циклів `for` / `while` / `do…while` в інтерпретаторі з обходом дерева |

```php
$engine->setLimit('maxLoopIterations', 1_000_000);
```

Усі інші назви мовчки ігноруються; для довжини рядків, виведення консолі чи реального часу лімітів немає.

Ліміт циклів поки що застосовується не всюди. Цикли, що виконуються у байт-кодовій VM, зокрема цикли в тілах функцій і програмах верхнього рівня, які приймає компілятор байт-коду, не підраховуються. Тому `while (true) {}` працює, доки його не зупинить PHP ([проблема #19](https://github.com/inline0/phasis/issues/19)). Не покладайтеся на цей ліміт для зупинки ненадійного коду; використовуйте PHP — `max_execution_time` або окремий процес.

Коли ліміт досягнуто, помилку можна перехопити за допомогою JS `try` / `catch` (як `Error` із повідомленням «Перевищено максимальну кількість ітерацій циклу»). Якщо її ніщо не перехопить, вона надійде до PHP як `Phasis\Exceptions\InternalError`, що успадковує `Phasis\Exceptions\RuntimeError`, а не `JsThrowable`.

У рушій вбудовано ще два ліміти, які не можна змінити під час виконання: `Phasis\Runtime\CallStack::maxDepth` (4,096 фреймів JS) і захист глибини транспільованих замикань JsToPhp (8,192 фреймів PHP). У разі перевищення обидва генерують помилку «Перевищено максимальний розмір стека викликів», яка також надходить до PHP як `InternalError`. Їхні значення значно перевищують потреби будь-якої легітимної програми JS; їх можна збільшити зміною коду, а не параметра конфігурації.

## скидання

```php
public function reset(): void
```

Очищає всі визначені користувачем глобальні змінні й буфер консолі, а також повторно створює стандартну бібліотеку та цикл подій.

У рушії, створеному в типовому лінивому режимі, `reset()` наразі втрачає всі ліниво встановлені вбудовані засоби: після цього `Map`, `JSON`, `Math`, `console`, `fetch` та решта стають невизначеними ([проблема #22](https://github.com/inline0/phasis/issues/22)). Поки це не виправлено, створіть натомість новий `Engine` або створіть його з `eager: true`, де `reset()` працює.

## Цикл подій

Рушій постачається з циклом подій JavaScript, що містить `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` і `queueMicrotask` у глобальному об’єкті. Мікрозавдання (обробники Promise, `queueMicrotask`) виконуються між кожними макрозавданнями; таймери спрацьовують у порядку дедлайнів через чергу завдань окремого середовища.

```php
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
```

`eval()` і `evalAsModule()` автоматично викликають `runEventLoop()` після завершення синхронного коду. Тож скрипт, який на верхньому рівні планує `setTimeout(cb, 5000)`, виконує `cb` до повернення `eval()` — цикл використовує **віртуальний час** і переводить годинник на наступний очікуваний дедлайн, а не чекає PHP. Відповідає семантиці `node script.js`.

Для вбудовування в реальному часі (довготривалі сервери, інтеграція UI, планування за реальним годинником) `tickEventLoop()` виконує одну ітерацію (обробляє мікрозавдання, запускає всі таймери, час яких настав) і повертає керування. `pendingTaskCount()` повідомляє про таймери, що залишилися; вбудовувач може керувати власним циклом:

```php
while ($engine->pendingTaskCount() > 0) {
    $engine->tickEventLoop();
    usleep(1000);
}
```

Нескінченний цикл `setInterval` має обмеження: `runEventLoop()` обмежує виконання 100,000 циклами й генерує звичайну `\RuntimeException` — помилка коду JS стає зрозумілою помилкою PHP, а не зависанням. Інтервал залишається запланованим, тож кожен наступний `eval()` у тому самому рушії генерує ту саму помилку; після цього відкиньте рушій. Необроблені помилки в зворотному виклику таймера надсилаються через `error_log()`, а цикл продовжує обробку; один несправний таймер не зупиняє програму.

## Перехоплювачі Fetch

Phasis постачається з увімкненим за замовчуванням `fetch()`, де транспортом слугує `ext-curl` від PHP. Три перехоплювачі дають змогу вбудовувачу замінити транспорт, контролювати запити або ввімкнути зберігання cookie. Усі три приймають викликаний об’єкт PHP або метод об’єкта; опис вимог до форми значень див. у розділі [Веб-API](https://phasis.inline0.dev/uk/docs/compatibility/web-apis).

### setFetchTransport

```php
public function setFetchTransport(callable $transport): void
```

Замінює CurlTransport за замовчуванням. Викликаний об’єкт отримує дескриптор запиту й необов’язковий JsObject `AbortSignal`; повертає дескриптор відповіді.

```php
// Use Guzzle as the transport instead of curl
$client = new GuzzleHttp\Client();
$engine->setFetchTransport(function (array $req, $signal) use ($client) {
    $r = $client->request($req['method'], $req['url'], [
        'headers' => $req['headers'],
        'body' => $req['body'],
        'http_errors' => false,
    ]);
    return [
        'status' => $r->getStatusCode(),
        'statusText' => $r->getReasonPhrase(),
        'headers' => array_map(
            fn ($name, $values) => array_map(fn ($v) => [$name, $v], $values),
            array_keys($r->getHeaders()),
            $r->getHeaders(),
        ),
        'body' => (string) $r->getBody(),
    ];
});
```

Типовий спосіб вбудовування: встановити підроблений транспорт у тестах і справжній транспорт curl у робочому середовищі.

### setFetchPolicy

```php
public function setFetchPolicy(callable $hook): void
```

Перехоплювач політики перед надсиланням запиту. Отримує JsObject `Request`; може згенерувати помилку, щоб відхилити запит, повернути змінений `Request` для перезапису або повернути `null` чи нічого, щоб дозволити запит.

```php
// Block non-HTTPS and inject auth on a specific host
$engine->setFetchPolicy(function ($req) {
    $url = $req->get('url')->value;
    if (str_starts_with($url, 'http://')) {
        throw new \RuntimeException('HTTPS only');
    }
    if (str_contains($url, 'api.example.com')) {
        $req->get('headers')->call('set', 'Authorization', 'Bearer ...');
    }
    return null;
});
```

### setCookieJar

```php
public function setCookieJar(mixed $jar): void
```

Сховище cookie, яке потрібно ввімкнути окремо. Приймає будь-який об’єкт із `get(string $url): string` і `set(string $url, string $header): void` — на боці PHP або JS.

```php
class SimpleJar
{
    private array $cookies = [];
    public function get(string $url): string { return implode('; ', $this->cookies); }
    public function set(string $url, string $header): void { $this->cookies[] = $header; }
}
$engine->setCookieJar(new SimpleJar());
```

Без `setCookieJar()` `fetch` ігнорує як вхідні `Set-Cookie`, так і вихідні заголовки `Cookie` — за замовчуванням **cookie не використовуються**.

## Винятки

| Клас | Наслідує | Коли виникає |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | явний JS `throw`, який нічим не перехоплено |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | помилка, яку сам рушій згенерував (наприклад, `null.x` або звернення до невизначеної змінної) і яку нічим не перехоплено |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | перевищено ліміт циклу або максимальну глибину стеку викликів |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | базовий клас для наведених вище |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | не вдалося розібрати вихідний код |
| `\RuntimeException` | | `execFile()` не вдалося прочитати файл; цикл подій досяг ліміту 100,000 циклів |

Щоб перехопити всі помилки скрипту, перехоплюйте `Phasis\Exceptions\RuntimeError` і `Phasis\Exceptions\SyntaxError` або просто `\RuntimeException`.

Винятки, згенеровані власними хост-функціями PHP, обробляються інакше: наразі вони проходять повз JS `try` / `catch` і без змін доходять до того, хто викликав `eval()`, зі своїм початковим класом ([помилка № 20](https://github.com/inline0/phasis/issues/20)). Див. розділ [Хост-функції](https://phasis.inline0.dev/uk/docs/interop/host-functions#throwing-back-into-js).

## Життєвий цикл

Створення `Engine` обходиться дешево (кілька мс під час першого запуску), а стандартна бібліотека завантажується за потреби. Для більшості сценаріїв вбудовування достатньо одного довготривалого рушія на процес. Для роботи з кількома клієнтами створюйте окремий рушій для кожного клієнта або запиту, щоб глобальні змінні між ними не передавалися. Наразі надійний спосіб почати з чистого стану — створити новий рушій (див. [скидання](#reset)).

```php
foreach ($requests as $req) {
    $engine = new Engine();
    $engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
    $engine->execFile($req->scriptPath);
}
```
