
# API

Interfejs wykonawczy API to jedna klasa: `Phasis\Engine`. Wszystko inne (`JsValue`, `JsFunction`, `JsObject`, …) jest wewnętrzne dla silnika i udostępniane wyłącznie funkcjom hosta łączącym PHP↔JS. Jedynym innym publicznym interfejsem jest samodzielny parser [parser API](https://phasis.inline0.dev/pl/docs/parser) (klasy `Phasis\Parser\Parser` i `Phasis\Ast`), który analizuje kod źródłowy bez jego wykonywania.

## Tworzenie

```php
use Phasis\Engine;

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

Nowy `Engine` udostępnia pełną bibliotekę standardową — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` oraz podstawowe elementy języka są instalowane od razu, aby specyfikacja działała (klasy wymagają `Function.prototype`, async/await — `Promise`, a literały wyrażeń regularnych — `RegExp`). Wszystkie pozostałe elementy — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` i rodzina array-buffer, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, rodzina weak-ref, `Temporal`, `ShadowRealm`, Web Platform Pack oraz Fetch Pack — są rejestrowane jako akcesory zastępcze i tworzone przy pierwszym odczycie. Odczyt zmiennej globalnej jest przezroczysty: działają `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')`, a po pierwszym odczycie akcesor zastępczy zostaje zastąpiony zwykłym deskryptorem danych.

Przekaż `eager: true`, jeśli potrzebujesz zachowania sprzed wprowadzenia leniwego ładowania — wszystkich funkcji wbudowanych instalowanych podczas tworzenia, z pełnym zestawem deskryptorów widocznym podczas introspekcji. Harness zgodności (`bin/test262`, `bin/wpt`) tworzy silniki w trybie eager, aby test262/WPT widziały oczekiwane kształty deskryptorów.

## eval

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

Analizuje i wykonuje `$source` jako klasyczny skrypt w globalnym zakresie silnika, a następnie uruchamia pętlę zdarzeń, aż zostanie opróżniona. Zwraca wartość ostatniego obliczonego wyrażenia przekonwertowaną na PHP (lub `null`, jeśli program nie kończył się wyrażeniem). Zasady opisano w sekcji [Konwersja wartości](https://phasis.inline0.dev/pl/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 nie jest rozpakowywany: `eval('Promise.resolve(5)')` zwraca pustą tablicę. Ponieważ pętla zdarzeń zostaje opróżniona, zanim `eval()` zwróci wynik, zapisz rozstrzygniętą wartość w zmiennej globalnej i odczytaj ją drugim wywołaniem `eval()`:

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

Jawne JS `throw`, którego nic nie przechwytuje, pojawia się jako `Phasis\Exceptions\JsThrowable`; jego komunikat to `"Name: message"`, a właściwość `$jsValue` przechowuje rzucone `JsValue`. Błędy zgłaszane przez sam silnik, takie jak odczyt właściwości `null` lub niezdefiniowanej zmiennej, pojawiają się zamiast tego jako `Phasis\Exceptions\TypeError`, `ReferenceError` lub `RangeError`. Wszystkie te klasy rozszerzają `Phasis\Exceptions\RuntimeError`; błędy parsowania rozszerzają `Phasis\Exceptions\SyntaxError`. Zobacz sekcję [Wyjątki](#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
```

Oblicza `$source` jako moduł ES: dostępne są `import` / `export`, najwyższopoziomowe `await` i `import.meta`, a specyfikatory względne są rozwiązywane względem `$path` (bezwzględnej ścieżki pliku). Uruchamia pętlę zdarzeń tak jak `eval()` i zawsze zwraca `null`; wyniki odczytuj przez zmienne globalne lub bufor konsoli.

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

Loader rozwiązuje wyłącznie ścieżki względne i bezwzględne; zobacz sekcję [Znane ograniczenia](https://phasis.inline0.dev/pl/docs/compatibility/limitations#module-loader).

## execFile

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

Odczytuje plik z dysku i przekazuje jego zawartość do `eval()`, więc plik jest uruchamiany jako klasyczny skrypt niezależnie od rozszerzenia; plik z `import` / `export` powoduje błąd SyntaxError. Rzeczywista ścieżka pliku staje się bazą dla dynamicznych specyfikatorów `import()`. W przypadku plików modułów użyj `evalAsModule()`.

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

Jeśli nie można odczytać pliku, PHP wyświetla ostrzeżenie, a `execFile()` zgłasza zwykły `\RuntimeException` ("Cannot read file: …").

## setGlobal

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

Wiąże zmienną globalną widoczną we wszystkich kolejnych wywołaniach JS. Wartości PHP są konwertowane na JS:

| Typ PHP | Typ JS |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (nawet gdy oznacza funkcję, taką jak `'strlen'`) |
| array (lista) | `Array` (kopia) |
| array (assoc) | `Object` (kopia) |
| `Closure`, obiekt wywoływalny, `[$object, 'method']` | `function` |
| każdy inny obiekt, generator lub zasób | `undefined` |

Tablice są kopiowane: zmiany wprowadzane w nich przez JS nie są widoczne w tablicy PHP. Zwykłych obiektów PHP nie można jeszcze przekazywać ([issue #18](https://github.com/inline0/phasis/issues/18)); obejścia opisano w sekcji [Obiekty współdzielone](https://phasis.inline0.dev/pl/docs/interop/shared-objects).

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

Wewnątrz JS:

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

## call

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

Wywołuje globalną funkcję JS z argumentami PHP (konwertowanymi tak jak w `setGlobal`). Wartość zwracana jest konwertowana z powrotem na PHP. Wywołanie niezdefiniowanej nazwy zgłasza `Phasis\Exceptions\ReferenceError`; wywołanie zmiennej globalnej, która nie jest funkcją, zgłasza `Phasis\Exceptions\TypeError`.

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

## Wyjście konsoli

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

`console.log`, `console.info`, `console.warn`, `console.error` i `console.debug` niczego nie wyświetlają. Każde wywołanie dopisuje jedną sformatowaną linię do bufora silnika. `getConsoleLines()` zwraca buforowane linie jako listę ciągów, `getConsoleOutput()` zwraca je połączone za pomocą `\n`, a `clearConsole()` opróżnia bufor. Bufor nie jest czyszczony między wywołaniami `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
```

Ustawia limit zasobów regulowany w czasie działania. Jest dokładnie jeden:

| Limit | Domyślnie | Opis |
|---|---|---|
| `maxLoopIterations` | `100000` | maksymalna liczba iteracji na przebieg pętli `for` / `while` / `do…while` wykonywanej przez interpreter przechodzący po drzewie |

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

Każda inna nazwa jest po cichu ignorowana; nie ma limitów długości ciągów, wyjścia konsoli ani czasu rzeczywistego.

Limit pętli nie jest jeszcze egzekwowany wszędzie. Pętle wykonywane na maszynie wirtualnej kodu bajtowego, obejmującej treści funkcji i programy najwyższego poziomu akceptowane przez kompilator kodu bajtowego, nigdy nie są zliczane, więc `while (true) {}` działa, dopóki nie zatrzyma go PHP ([issue #19](https://github.com/inline0/phasis/issues/19)). Nie polegaj na tym mechanizmie, aby zatrzymywać niezaufany kod; użyj PHP z `max_execution_time` lub osobnego procesu.

Po osiągnięciu limitu błąd można przechwycić za pomocą JS `try` / `catch` (jako `Error` z komunikatem "Maximum loop iterations exceeded"). Jeśli nic go nie przechwyci, trafia do PHP jako `Phasis\Exceptions\InternalError`, która rozszerza `Phasis\Exceptions\RuntimeError`, a nie `JsThrowable`.

Dwa dodatkowe limity są na stałe wbudowane w silnik i nie można ich regulować w czasie działania: `Phasis\Runtime\CallStack::maxDepth` (ramki 4,096 JS) oraz limit głębokości domknięć transpilowanych przez JsToPhp (ramki 8,192 PHP). Po przekroczeniu obu zgłaszany jest błąd "Maximum call stack size exceeded", który również trafia do PHP jako `InternalError`. Limity są znacznie wyższe niż potrzeba jakiemukolwiek prawidłowemu programowi JS — ich podniesienie wymaga zmiany kodu, a nie ustawienia konfiguracji.

## reset

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

Czyści wszystkie zmienne globalne zdefiniowane przez użytkownika i bufor konsoli oraz odbudowuje bibliotekę standardową i pętlę zdarzeń.

W silniku utworzonym w domyślnym trybie lazy funkcja `reset()` obecnie usuwa wszystkie leniwie instalowane funkcje wbudowane: później `Map`, `JSON`, `Math`, `console`, `fetch` i pozostałe są niezdefiniowane ([issue #22](https://github.com/inline0/phasis/issues/22)). Dopóki nie zostanie to naprawione, utwórz nowy `Engine` albo utwórz go z `eager: true`, gdzie działa `reset()`.

## Pętla zdarzeń

Silnik zawiera własną pętlę zdarzeń JavaScript z `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` i `queueMicrotask` na obiekcie globalnym. Mikro-zadania (procedury obsługi Promise, `queueMicrotask`) są realizowane między każdym makrozadaniem; timery uruchamiają się według terminów za pośrednictwem kolejki zadań przypisanej do danej domeny.

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

`eval()` i `evalAsModule()` automatycznie wywołują `runEventLoop()` po zakończeniu kodu synchronicznego. Skrypt, który na najwyższym poziomie zaplanuje `setTimeout(cb, 5000)`, wykona więc `cb` przed zwróceniem wyniku przez `eval()` — pętla używa **czasu wirtualnego** i przestawia zegar na kolejny oczekujący termin zamiast spać przez PHP. Zgodne z semantyką `node script.js`.

W przypadku osadzania w czasie rzeczywistym (serwery działające długotrwale, integracja UI, harmonogramowanie według rzeczywistego zegara) `tickEventLoop()` wykonuje jedną iterację (opróżnia mikro-zadania, uruchamia należne timery) i zwraca wynik. `pendingTaskCount()` informuje o oczekujących timerach; kod osadzający może obsługiwać własną pętlę:

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

Niekontrolowana pętla `setInterval` ma ograniczenie: `runEventLoop()` zatrzymuje się po 100,000 cyklach i zgłasza zwykły `\RuntimeException` — wina kodu JS jest sygnalizowana czytelnym błędem PHP, a nie zawieszeniem. Interwał pozostaje zaplanowany, więc każde późniejsze wywołanie `eval()` na tym samym silniku zgłosi ten sam błąd; później odrzuć silnik. Nieprzechwycone błędy w wywołaniu zwrotnym timera są emitowane przez `error_log()`, a pętla kontynuuje pracę; pojedynczy wadliwy timer nie zatrzymuje programu.

## Haki Fetch

Phasis domyślnie dostarcza włączoną funkcję `fetch()`, używającą `ext-curl` z PHP jako transportu bazowego. Trzy haki pozwalają zastąpić transport, filtrować żądania lub włączyć przechowywanie ciasteczek. Każdy z nich akceptuje wywoływalny PHP albo metodę obiektu; opis dozwolonych kształtów wartości znajduje się w sekcji [Interfejsy Web](https://phasis.inline0.dev/pl/docs/compatibility/web-apis).

### setFetchTransport

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

Zastępuje domyślny CurlTransport. Wywoływalny element otrzymuje deskryptor żądania i opcjonalny `AbortSignal` JsObject; zwraca deskryptor odpowiedzi.

```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(),
    ];
});
```

Częsty wzorzec osadzania: instalacja mockowanego transportu w testach i prawdziwego transportu curl na produkcji.

### setFetchPolicy

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

Hak zasad przed wysłaniem żądania. Otrzymuje `Request` JsObject; może zgłosić wyjątek, aby odmówić, zwrócić zmodyfikowany `Request`, aby przepisać żądanie, lub zwrócić `null`/brak wartości, aby zezwolić.

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

Opcjonalny magazyn ciasteczek. Akceptuje dowolny obiekt udostępniający `get(string $url): string` i `set(string $url, string $header): void` — po stronie PHP lub 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());
```

Bez `setCookieJar()` `fetch` ignoruje zarówno przychodzące, jak i wychodzące nagłówki `Set-Cookie` i `Cookie` — domyślnie **nie ma ciasteczek**.

## Wyjątki

| Klasa | Rozszerza | Kiedy |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | jawne JS `throw`, którego nic nie przechwyciło |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | błąd zgłoszony przez silnik (na przykład `null.x` lub niezdefiniowana zmienna), którego nic nie przechwyciło |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | przekroczono limit pętli lub limit stosu wywołań |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | klasa bazowa powyższych |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | kod źródłowy nie daje się sparsować |
| `\RuntimeException` | | `execFile()` nie może odczytać pliku; pętla zdarzeń osiągnęła limit 100,000 cykli |

Aby przechwycić każdy błąd skryptu, przechwytuj `Phasis\Exceptions\RuntimeError` i `Phasis\Exceptions\SyntaxError` albo po prostu `\RuntimeException`.

Wyjątki zgłaszane przez własne funkcje hosta PHP są inne: obecnie przechodzą przez JS `try` / `catch` i docierają bez zmian do wywołującego `eval()` wraz z pierwotną klasą ([issue #20](https://github.com/inline0/phasis/issues/20)). Zobacz sekcję [Funkcje hosta](https://phasis.inline0.dev/pl/docs/interop/host-functions#throwing-back-into-js).

## Cykl życia

Tworzenie `Engine` jest tanie (kilka ms przy zimnym starcie), a biblioteka standardowa jest instalowana leniwie. W większości przypadków osadzania wystarczy jeden długotrwale działający silnik na proces. Przy użyciu wielu dzierżawców utwórz osobny silnik dla każdego z nich lub dla każdego żądania, aby zmienne globalne nie wyciekały między nimi. Obecnie niezawodny czysty stan zapewnia utworzenie nowego silnika (zobacz [reset](#reset)).

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