
# API

У выполнения API только один класс: `Phasis\Engine`. Всё остальное (`JsValue`, `JsFunction`, `JsObject`, …) относится к внутреннему устройству движка и доступно только хост-функциям, которые обеспечивают взаимодействие между PHP↔JS. Ещё одна публичная поверхность — отдельный [парсер API](https://phasis.inline0.dev/ru/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`, семейство буферов массивов, `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`) создаёт движки в режиме ранней инициализации, чтобы test262/WPT видели ожидаемые формы дескрипторов.

## вычисление

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

Разбирает и выполняет `$source` как классический скрипт в глобальной области видимости движка, а затем запускает цикл событий до его опустошения. Возвращает значение последнего вычисленного выражения, преобразованное в PHP (или `null`, если программа не заканчивается выражением). Правила описаны в разделе [Преобразование значений](https://phasis.inline0.dev/ru/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/ru/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` ("Cannot read file: …").

## 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/ru/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);
```

Любое другое имя молча игнорируется; ограничений на длину строк, вывод в консоль или время выполнения нет.

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

При достижении лимита ошибку можно перехватить с помощью JS `try` / `catch` (как `Error` с сообщением "Maximum loop iterations exceeded"). Если ее никто не перехватит, она дойдет до PHP как `Phasis\Exceptions\InternalError`, наследующий `Phasis\Exceptions\RuntimeError`, а не `JsThrowable`.

В движке зашиты еще два предела, которые нельзя настроить во время выполнения: `Phasis\Runtime\CallStack::maxDepth` (4,096 кадров JS) и ограничение глубины транспилированных замыканий JsToPhp (8,192 кадров PHP). При превышении обоих возникает ошибка "Maximum call stack size exceeded", которая также доходит до PHP как `InternalError`. Эти пределы намного выше потребностей любой допустимой программы на JS; увеличить их можно только изменением кода, настройки для этого нет.

## сброс

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

Удаляет все определенные пользователем глобальные переменные и буфер консоли, а также заново создает стандартную библиотеку и цикл событий.

Для движка, созданного в стандартном ленивом режиме, `reset()` сейчас удаляет все лениво установленные встроенные объекты: после этого `Map`, `JSON`, `Math`, `console`, `fetch` и остальные не определены ([issue #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/ru/docs/compatibility/web-apis).

### setFetchTransport

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

Заменяет CurlTransport по умолчанию. Функция получает дескриптор запроса и необязательный `AbortSignal` JsObject; возвращает дескриптор ответа.

```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
```

Перехватчик политики перед отправкой запроса. Получает `Request` JsObject; может выбросить ошибку, чтобы запретить запрос, вернуть измененный `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()` с исходным классом ([issue #20](https://github.com/inline0/phasis/issues/20)). См. раздел [Хост-функции](https://phasis.inline0.dev/ru/docs/interop/host-functions#throwing-back-into-js).

## Жизненный цикл

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

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