
# API

Изпълнителната API се състои от един клас: `Phasis\Engine`. Всичко останало (`JsValue`, `JsFunction`, `JsObject`, …) е вътрешна част на енджина и е достъпно само за хост функциите, които свързват PHP↔JS. Единствената друга публична повърхност е самостоятелният [парсер API](https://phasis.inline0.dev/bg/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`, async/await без `Promise` или regex литерали без `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`) създава енджините в нетърпелив режим, така че test262/WPT да виждат очакваните форми на дескрипторите.

## оценка

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

Парсва и изпълнява `$source` като класически скрипт в глобалния обхват на енджина, след което стартира цикъла на събитията, докато се изпразни. Връща стойността на последния оценен израз, преобразувана в PHP (или `null`, ако програмата няма завършващ израз). Правилата вижте в [Преобразуване на стойности](https://phasis.inline0.dev/bg/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/bg/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/bg/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` със съобщение "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. Три обработчика позволяват на вграждащия код да смени транспорта, да контролира заявките или да включи съхранението на бисквитки. И трите приемат извикваем обект от тип PHP или метод на обект; вижте [Уеб API](https://phasis.inline0.dev/bg/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
```

Хранилище за бисквитки, което се включва изрично. Приема всеки обект с `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` — по подразбиране **няма бисквитки**.

## Изключения

| Клас | Наследява | Кога |
|---|---|---|
| `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/bg/docs/interop/host-functions#throwing-back-into-js).

## Жизнен цикъл

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

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