
# API

Vmesnik za izvajanje API predstavlja en sam razred: `Phasis\Engine`. Vse drugo (`JsValue`, `JsFunction`, `JsObject`, …) je notranji del pogona in je na voljo samo gostiteljskim funkcijam, ki povezujejo PHP↔JS. Edini drug javni vmesnik je samostojni razčlenjevalnik [parser API](https://phasis.inline0.dev/sl/docs/parser) (`Phasis\Parser\Parser` in razredi `Phasis\Ast`), ki razčleni izvorno kodo, ne da bi jo izvedel.

## Ustvarjanje

```php
use Phasis\Engine;

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

Svež `Engine` razkrije celotno standardno knjižnico — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` in osnovne jezikovne intrinzične funkcije se namestijo takoj, da specifikacija deluje (razredov ni mogoče uporabljati brez `Function.prototype`, async/await brez `Promise`, regularnih izrazov v literalih pa brez `RegExp`). Vse drugo — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` in družina medpomnilnikov polj, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, družina šibkih referenc, `Temporal`, `ShadowRealm`, Web Platform Pack in Fetch Pack — se registrira kot nadomestni dostopnik in ustvari ob prvem branju. Branje globalne spremenljivke je pregledno: `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')` vse deluje, po prvem branju pa se nadomestni dostopnik zamenja z običajnim deskriptorjem podatkov.

Podajte `eager: true`, če potrebujete vedenje pred uvedbo odloženega nalaganja — vse vgrajene funkcije so nameščene že ob ustvarjanju, introspekcija pa vidi celoten nabor deskriptorjev. Testno okolje za skladnost (`bin/test262`, `bin/wpt`) ustvarja pogone v takojšnjem načinu, da test262/WPT vidita pričakovane oblike deskriptorjev.

## eval

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

Razčleni in izvede `$source` kot klasični skript v globalnem obsegu pogona, nato pa izvaja zanko dogodkov, dokler se ne izprazni. Vrne vrednost zadnjega ovrednotenega izraza, pretvorjeno v PHP (ali `null`, če program nima zaključnega izraza). Pravila so opisana v razdelku [Pretvorba vrednosti](https://phasis.inline0.dev/sl/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)
```

Obljuba se ne razreši samodejno: `eval('Promise.resolve(5)')` vrne prazno polje. Ker je zanka dogodkov izpraznjena, ko se `eval()` vrne, shranite razrešeno vrednost v globalno spremenljivko in jo preberite z drugim klicem `eval()`:

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

Izrecni JS `throw`, ki ga nič ne prestreže, se prikaže kot `Phasis\Exceptions\JsThrowable`; njegovo sporočilo je `"Name: message"`, njegova lastnost `$jsValue` pa vsebuje sproženi `JsValue`. Napake, ki jih sproži sam pogon, na primer branje lastnosti vrednosti `null` ali nedefinirane spremenljivke, se namesto tega prikažejo kot `Phasis\Exceptions\TypeError`, `ReferenceError` ali `RangeError`. Vse te napake dedujejo od `Phasis\Exceptions\RuntimeError`; napake pri razčlenjevanju dedujejo od `Phasis\Exceptions\SyntaxError`. Glejte razdelek [Izjeme](#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
```

Ovrednoti `$source` kot modul ES: na voljo sta `import` / `export`, deklaraciji `await` in `import.meta` na vrhnji ravni, relativni označevalniki pa se razrešijo glede na `$path` (absolutno pot do datoteke). Zanko dogodkov izvede tako kot `eval()` in vedno vrne `null`; rezultate preberite prek globalnih spremenljivk ali medpomnilnika konzole.

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

Nalagalnik razreši samo relativne in absolutne poti; glejte [Znane omejitve](https://phasis.inline0.dev/sl/docs/compatibility/limitations#module-loader).

## execFile

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

Prebere datoteko z diska in njeno vsebino posreduje funkciji `eval()`, zato se izvede kot klasični skript ne glede na končnico; datoteka z `import` / `export` povzroči SyntaxError. Dejanska pot do datoteke postane osnova za dinamične označevalnike `import()`. Za datoteke modulov uporabite `evalAsModule()`.

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

Če datoteke ni mogoče prebrati, PHP izpiše opozorilo, `execFile()` pa sproži navadni `\RuntimeException` (»Cannot read file: …«).

## setGlobal

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

Veže globalno spremenljivko, ki je vidna vsem poznejšim ovrednotenjem JS. Vrednosti PHP se pretvorijo v JS:

| Vrsta PHP | Vrsta JS |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (tudi kadar poimenuje funkcijo, kot je `'strlen'`) |
| polje (seznam) | `Array` (kopija) |
| polje (asociativno) | `Object` (kopija) |
| `Closure`, klicljiv objekt, `[$object, 'method']` | `function` |
| kateri koli drug objekt, generator ali vir | `undefined` |

Polja se kopirajo: spremembe, ki jih v njih naredi JS, niso vidne v polju PHP. Navadnih objektov PHP še ni mogoče posredovati ([težava št. 18](https://github.com/inline0/phasis/issues/18)); za rešitve glejte razdelek [Skupni objekti](https://phasis.inline0.dev/sl/docs/interop/shared-objects).

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

Znotraj JS:

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

## call

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

Pokliče globalno funkcijo JS z argumenti PHP (pretvorjenimi enako kot pri `setGlobal`). Vrnjena vrednost se pretvori nazaj v PHP. Klic nedefiniranega imena sproži `Phasis\Exceptions\ReferenceError`; klic globalne spremenljivke, ki ni funkcija, pa sproži `Phasis\Exceptions\TypeError`.

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

## Izpis konzole

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

`console.log`, `console.info`, `console.warn`, `console.error` in `console.debug` ne izpisujejo ničesar. Vsak klic doda eno oblikovano vrstico v medpomnilnik pogona. `getConsoleLines()` vrne shranjene vrstice kot seznam nizov, `getConsoleOutput()` jih vrne združene z `\n`, `clearConsole()` pa izprazni medpomnilnik. Med klici `eval()` se medpomnilnik ne izbriše.

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

Nastavi omejitev virov, ki jo je mogoče spreminjati med izvajanjem. Obstaja samo ena:

| Omejitev | Privzeto | Opis |
|---|---|---|
| `maxLoopIterations` | `100000` | največje število ponovitev na zagon zanke `for` / `while` / `do…while` v tolmaču, ki prehaja drevo |

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

Vsako drugo ime se tiho prezre; omejitev za dolžino nizov, izpis konzole ali čas izvajanja ni.

Omejitev zank še ni uveljavljena povsod. Zanke, ki se izvajajo v navideznem stroju bajtne kode — ta zajema telesa funkcij in programe na vrhnji ravni, ki jih sprejme prevajalnik bajtne kode — se nikoli ne štejejo, zato se `while (true) {}` izvaja, dokler ga PHP ne ustavi ([težava št. 19](https://github.com/inline0/phasis/issues/19)). Ne zanašajte se na to, da bo omejitev ustavila nezaupanja vredno kodo; uporabite PHP `max_execution_time` ali ločen proces.

Ko je omejitev dosežena, lahko napako prestreže blok JS `try` / `catch` (kot `Error` s sporočilom »Maximum loop iterations exceeded«). Če je nič ne prestreže, doseže PHP kot `Phasis\Exceptions\InternalError`, ki deduje od `Phasis\Exceptions\RuntimeError`, ne od `JsThrowable`.

V pogon sta vgrajeni še dve omejitvi, ki ju ni mogoče spreminjati med izvajanjem: `Phasis\Runtime\CallStack::maxDepth` (okviri 4,096 JS) in omejitev globine transpilanih zaprtij JsToPhp (okviri 8,192 PHP). Pri prekoračitvi obe sprožita »Maximum call stack size exceeded«, kar se prav tako prikaže v PHP kot `InternalError`. Njuni vrednosti sta precej višji od tistih, ki jih potrebuje kateri koli običajen program JS — povečati ju je mogoče s spremembo kode, ne nastavitve.

## reset

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

Izbriše vse uporabniško določene globalne spremenljivke in medpomnilnik konzole ter znova ustvari standardno knjižnico in zanko dogodkov.

Pri pogonu, ustvarjenem v privzetem odloženem načinu, `reset()` trenutno odstrani vse vgrajene funkcije, nameščene z odloženim nalaganjem: nato `Map`, `JSON`, `Math`, `console`, `fetch` in vse druge niso definirane ([težava št. 22](https://github.com/inline0/phasis/issues/22)). Dokler težava ni odpravljena, raje ustvarite nov `Engine` ali pa ga ustvarite z `eager: true`, kjer `reset()` deluje.

## Zanka dogodkov

Pogon vključuje zanko dogodkov JavaScript z globalnim objektom, ki vsebuje `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` in `queueMicrotask`. Mikroopravila (obravnavalniki Promise, `queueMicrotask`) se izpraznijo po vsakem makroopravilu; časovniki se prožijo po vrstnem redu rokov prek čakalne vrste opravil posameznega področja.

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

`eval()` in `evalAsModule()` samodejno pokličeta `runEventLoop()` po zaključku sinhrone kode. Skript, ki na vrhnji ravni načrtuje `setTimeout(cb, 5000)`, se zato izvede `cb`, preden se `eval()` vrne — zanka uporablja **navidezni čas** in uro premakne na naslednji čakajoči rok, namesto da bi čakala PHP. Ustreza semantiki `node script.js`.

Za vdelavo v sisteme z realnim časom (dolgotrajni strežniki, integracija UI, načrtovanje glede na dejansko uro) `tickEventLoop()` izvede eno iteracijo (izprazni mikroopravila, sproži zapadle časovnike) in se vrne. `pendingTaskCount()` poroča o čakajočih časovnikih; gostiteljska aplikacija lahko upravlja lastno zanko:

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

Neskončno izvajanje `setInterval` je omejeno: `runEventLoop()` se ustavi pri 100,000 ciklih in sproži navadni `\RuntimeException` — napaka v kodi JS se jasno prikaže kot napaka PHP, namesto da bi se izvajanje zataknilo. Interval ostane načrtovan, zato vsak naslednji `eval()` v istem pogonu sproži isto napako; pogon zatem zavrzite. Neprestrežene napake v povratnem klicu časovnika se oddajo prek `error_log()`, zanka pa nadaljuje izvajanje; ena napačna funkcija časovnika ne ustavi programa.

## Kljuke za Fetch

Phasis privzeto vključuje `fetch()`, ki kot osnovni prenosni mehanizem uporablja `ext-curl` iz PHP. Tri kljuke gostiteljski aplikaciji omogočajo zamenjavo prenosnega mehanizma, omejevanje zahtevkov ali vklop shranjevanja piškotkov. Vsaka sprejme klicni objekt PHP ali metodo objekta; opis oblike vrednosti je v razdelku [Spletni API-ji](https://phasis.inline0.dev/sl/docs/compatibility/web-apis).

### setFetchTransport

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

Zamenja privzeti CurlTransport. Klicni objekt prejme opis zahtevka in izbirni `AbortSignal` JsObject; vrne opis odgovora.

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

Pogost vzorec vdelave: v testih namestite ponarejeni prenosni mehanizem, v produkciji pa pravi prenosni mehanizem curl.

### setFetchPolicy

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

Kljuka pravilnika pred pošiljanjem zahtevka. Prejme `Request` JsObject; lahko sproži izjemo za zavrnitev, vrne spremenjeni `Request` za preoblikovanje ali vrne `null`/ne vrne vrednosti, da zahtevek dovoli.

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

Shramba piškotkov, ki jo je treba izrecno vklopiti. Sprejme kateri koli objekt z metodama `get(string $url): string` in `set(string $url, string $header): void` — na strani PHP ali 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());
```

Brez `setCookieJar()` `fetch` prezre tako dohodne glave `Set-Cookie` kot odhodne glave `Cookie` — privzeto se piškotki ne uporabljajo.

## Izjeme

| Razred | Deduje od | Kdaj |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | izrecni JS `throw`, ki ga nič ne prestreže |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | napaka, ki jo sproži sam pogon (npr. `null.x` ali nedefinirana spremenljivka) in je nič ne prestreže |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | presežena je omejitev zanke ali velikosti sklada klicev |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | osnovni razred za zgornje razrede |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | izvorne kode ni mogoče razčleniti |
| `\RuntimeException` | | `execFile()` ne more prebrati datoteke; zanka dogodkov je dosegla omejitev 100,000 ciklov |

Če želite prestreči vse napake skripta, prestrezite `Phasis\Exceptions\RuntimeError` in `Phasis\Exceptions\SyntaxError` ali preprosto `\RuntimeException`.

Izjeme, ki jih sprožijo lastne gostiteljske funkcije PHP, so drugačne: trenutno se nespremenjene prebijejo skozi JS `try` / `catch` do klicatelja funkcije `eval()`, ohranijo pa svoj izvirni razred ([težava št. 20](https://github.com/inline0/phasis/issues/20)). Glejte razdelek [Gostiteljske funkcije](https://phasis.inline0.dev/sl/docs/interop/host-functions#throwing-back-into-js).

## Življenjski cikel

`Engine` je poceni za ustvarjanje (nekaj ms pri hladnem zagonu), standardna knjižnica pa se namešča z odloženim nalaganjem. Za večino vdelav je primeren en dolgotrajno delujoč pogon na proces. Pri uporabi za več najemnikov vsakemu najemniku ali zahtevku dodelite lasten pogon, da se globalne spremenljivke med njimi ne prenašajo. Zanesljiv način za čist začetek je danes ustvarjanje novega pogona (glejte [reset](#reset)).

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