
# API

Suorituksen API on yksi luokka: `Phasis\Engine`. Kaikki muu (`JsValue`, `JsFunction`, `JsObject`, …) on moottorin sisäistä ja näkyy vain isäntäfunktioille, jotka yhdistävät PHP↔JS. Ainoa muu julkinen rajapinta on itsenäinen [jäsennin API](https://phasis.inline0.dev/fi/docs/parser) (`Phasis\Parser\Parser`- ja `Phasis\Ast`-luokat), joka jäsentää lähdekoodin suorittamatta sitä.

## Luominen

```php
use Phasis\Engine;

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

Uusi `Engine` tarjoaa koko standardikirjaston — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` sekä kielen keskeiset sisäiset toiminnot asennetaan heti, jotta määritys toimii (luokkia ei voi käyttää ilman kohdetta `Function.prototype`, async/awaitia ilman kohdetta `Promise` eikä regex-literaaleja ilman kohdetta `RegExp`). Kaikki muu — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` ja array-buffer-perhe, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, weak-ref-perhe, `Temporal`, `ShadowRealm`, Web Platform Pack ja Fetch Pack — rekisteröidään paikkamerkkihakijana ja luodaan ensimmäisellä lukukerralla. Globaalin lukeminen on läpinäkyvää: `Map`, `typeof Map`, `'Map' in globalThis` ja `Object.keys(globalThis).includes('Map')` toimivat, ja ensimmäisen lukukerran jälkeen paikkamerkki korvataan tavallisella data-deskriptorilla.

Välitä `eager: true`, jos tarvitset nimenomaan toimintatavan ennen laiskaa alustusta — kaikki sisäänrakennetut toiminnot asennetaan luonnin yhteydessä ja koko kuvaileva pinta on nähtävissä tarkastelua varten. Vaatimustenmukaisuustestien ajoympäristö (`bin/test262`, `bin/wpt`) luo moottorit tässä välittömässä alustustilassa, jotta test262 ja WPT näkevät odottamansa deskriptorien muodot.

## eval

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

Jäsentää ja suorittaa kohteen `$source` klassisena skriptinä moottorin globaalissa käyttöalueessa ja suorittaa sitten tapahtumasilmukkaa, kunnes se on tyhjä. Palauttaa viimeksi arvioidun lausekkeen arvon muunnettuna muotoon PHP (tai `null`, jos ohjelman lopussa ei ollut lauseketta). Katso säännöt kohdasta [Arvojen muuntaminen](https://phasis.inline0.dev/fi/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)
```

Promisea ei pureta: `eval('Promise.resolve(5)')` palauttaa tyhjän taulukon. Koska tapahtumasilmukka on tyhjentynyt siihen mennessä, kun `eval()` palauttaa, tallenna ratkennut arvo globaaliin muuttujaan ja lue se toisella `eval()`-kutsulla:

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

Eksplisiittinen JS `throw`, jota mikään ei sieppaa, ilmenee muodossa `Phasis\Exceptions\JsThrowable`, jonka viesti on `"Name: message"` ja jonka `$jsValue`-ominaisuus sisältää heitetyn `JsValue`-arvon. Moottorin itse aiheuttamat virheet, kuten ominaisuuden lukeminen arvosta `null` tai määrittelemätön muuttuja, ilmenevät sen sijaan muodossa `Phasis\Exceptions\TypeError`, `ReferenceError` tai `RangeError`. Kaikki nämä perivät luokan `Phasis\Exceptions\RuntimeError`; jäsennysvirheet perivät luokan `Phasis\Exceptions\SyntaxError`. Katso [Poikkeukset](#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
```

Suorittaa `$source`-arvon ES-moduulina: `import` / `export`, ylimmän tason `await` ja `import.meta` ovat käytettävissä, ja suhteelliset määrittäjät ratkaistaan suhteessa arvoon `$path` (absoluuttinen tiedostopolku). Suorittaa tapahtumasilmukan kuten `eval()` ja palauttaa aina arvon `null`; lue tulokset globaaleista muuttujista tai konsolipuskurista.

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

Lataaja ratkaisee vain suhteelliset ja absoluuttiset polut; katso [Tunnetut rajoitukset](https://phasis.inline0.dev/fi/docs/compatibility/limitations#module-loader).

## execFile

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

Lukee tiedoston levyltä ja välittää sen sisällön arvolle `eval()`, joten se suoritetaan tavallisena skriptinä tiedostopäätteestä riippumatta; tiedosto, jossa on `import` / `export`, aiheuttaa SyntaxError-virheen. Tiedoston todellista polkua käytetään dynaamisten `import()`-määrittäjien perustana. Käytä moduulitiedostoille toimintoa `evalAsModule()`.

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

Jos tiedostoa ei voi lukea, PHP antaa varoituksen ja `execFile()` heittää tavallisen `\RuntimeException`-virheen ("Tiedostoa ei voi lukea: …").

## setGlobal

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

Sitoo globaalin muuttujan, joka näkyy kaikissa myöhemmissä JS-arvioinneissa. PHP-arvot muunnetaan tyypiksi JS:

| PHP-tyyppi | JS-tyyppi |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (myös silloin, kun se nimeää funktion, kuten `'strlen'`) |
| taulukko (lista) | `Array` (kopio) |
| taulukko (assosiatiivinen) | `Object` (kopio) |
| `Closure`, kutsuttava olio, `[$object, 'method']` | `function` |
| mikä tahansa muu olio, generaattori tai resurssi | `undefined` |

Taulukot kopioidaan: niiden muutokset, jotka JS tekee, eivät näy PHP-taulukossa. Tavallisia PHP-olioita ei vielä voi välittää ([ongelma #18](https://github.com/inline0/phasis/issues/18)); katso kiertotapoja kohdasta [Jaetut oliot](https://phasis.inline0.dev/fi/docs/interop/shared-objects).

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

Toiminnon JS sisällä:

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

## kutsu

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

Kutsuu globaalia JS-funktiota, jolle välitetään PHP-argumentit (muunnettuina kuten kohdassa `setGlobal`). Palautusarvo muunnetaan takaisin tyypiksi PHP. Määrittelemättömän nimen kutsuminen heittää virheen `Phasis\Exceptions\ReferenceError`; funktiona toimimattoman globaalin muuttujan kutsuminen heittää virheen `Phasis\Exceptions\TypeError`.

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

## Konsolituloste

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

`console.log`, `console.info`, `console.warn`, `console.error` ja `console.debug` eivät tulosta mitään. Kukin kutsu lisää moottorin puskuriin yhden muotoillun rivin. `getConsoleLines()` palauttaa puskuroidut rivit merkkijonoluettelona, `getConsoleOutput()` palauttaa ne arvolla `\n` yhdistettyinä, ja `clearConsole()` tyhjentää puskurin. Puskuria ei tyhjennetä `eval()`-kutsujen välillä.

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

Asettaa suorituksen aikana muutettavan resurssirajan. Niitä on tasan yksi:

| Raja | Oletusarvo | Kuvaus |
|---|---|---|
| `maxLoopIterations` | `100000` | puuta käyvän tulkin suorittamien `for` / `while` / `do…while` -silmukoiden enimmäisiterointimäärä |

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

Kaikki muut nimet ohitetaan hiljaisesti; merkkijonon pituudelle, konsolitulosteelle tai seinäkelloajalle ei ole rajoja.

Silmukkarajaa ei vielä valvota kaikkialla. Tavutavun virtuaalikoneessa suoritettavia silmukoita, eli funktioiden rungot ja tavukoodikääntäjän hyväksymät ylimmän tason ohjelmat, ei lasketa lainkaan, joten `while (true) {}` suoritetaan, kunnes PHP pysäyttää sen ([ongelma #19](https://github.com/inline0/phasis/issues/19)). Älä luota siihen epäluotetun koodin pysäyttämisessä; käytä PHP-ohjelman `max_execution_time`-toimintoa tai erillistä prosessia.

Rajan ylittyessä virheen voi siepata JS `try` / `catch` -rakenteella (virheenä `Error`, jonka viesti on "Silmukan iteraatioiden enimmäismäärä ylitetty"). Jos mikään ei sieppaa virhettä, se välittyy arvoon PHP muodossa `Phasis\Exceptions\InternalError`, joka perii luokan `Phasis\Exceptions\RuntimeError`, ei luokkaa `JsThrowable`.

Moottoriin on kovakoodattu kaksi lisärajaa, joita ei voi muuttaa suorituksen aikana: `Phasis\Runtime\CallStack::maxDepth` (4,096 JS kehystä) ja JsToPhp-muunnettujen sulkeumien syvyysraja (8,192 PHP kehystä). Kummankin ylittyessä heitetään virhe "Maximum call stack size exceeded", joka välittyy myös arvoon PHP muodossa `InternalError`. Rajat ovat selvästi suurempia kuin minkään kelvollisen JS-ohjelman tarpeet — niiden kasvattaminen vaatii koodimuutoksen, eikä onnistu asetuksella.

## nollaa

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

Tyhjentää kaikki käyttäjän määrittämät globaalit muuttujat ja konsolipuskurin sekä rakentaa vakiokirjaston ja tapahtumasilmukan uudelleen.

Oletusarvoisessa laiskassa tilassa rakennetusta moottorista `reset()` poistaa tällä hetkellä kaikki laiskasti asennetut sisäänrakennetut toiminnot: sen jälkeen `Map`, `JSON`, `Math`, `console`, `fetch` ja muut ovat määrittelemättömiä ([ongelma #22](https://github.com/inline0/phasis/issues/22)). Kunnes tämä on korjattu, rakenna sen sijaan uusi `Engine` tai rakenna se käyttäen asetusta `eager: true`, jolloin `reset()` toimii.

## Tapahtumasilmukka

Moottorin mukana toimitetaan JavaScript-tapahtumasilmukka, jossa globaalissa oliossa ovat `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` ja `queueMicrotask`. Mikrotehtävät (Promise-käsittelijät, `queueMicrotask`) käsitellään jokaisen makrotehtävän välillä; ajastimet laukeavat määräaikajärjestyksessä aluekohtaisen tehtäväjonon kautta.

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

`eval()` ja `evalAsModule()` kutsuvat `runEventLoop()`-toimintoa automaattisesti synkronisen koodin suorituksen jälkeen. Siksi ylimmällä tasolla ajastuksen `setTimeout(cb, 5000)` asettava skripti suorittaa sen ennen kuin `eval()` palauttaa arvon `cb` — silmukka käyttää **virtuaaliaikaa** ja siirtää kelloaan seuraavaan odottavaan määräaikaan sen sijaan, että odottaisi PHP. Vastaa `node script.js`-semantiikkaa.

Reaaliaikaista upotusta varten (pitkäikäiset palvelimet, UI-integraatio, aikataulutus todellisen kellon mukaan) `tickEventLoop()` suorittaa yhden iteraation (käsittelee mikrotehtävät ja laukaisee erääntyneet ajastimet) ja palauttaa ohjauksen. `pendingTaskCount()` ilmoittaa odottavien ajastimien määrän; upottaja voi ajaa omaa silmukkaansa:

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

Karkaava `setInterval` on rajattu: `runEventLoop()` pysähtyy 100,000 syklin jälkeen ja heittää tavallisen `\RuntimeException`-virheen — koodin JS vika näkyy selvänä PHP-virheenä jumiutumisen sijasta. Aikaväli pysyy ajastettuna, joten jokainen myöhempi `eval()` samassa moottorissa heittää saman virheen; hylkää moottori sen jälkeen. Ajastinkutsun takaisinkutsussa sieppaamattomat virheet ilmoitetaan toiminnon `error_log()` kautta, ja silmukka jatkaa tehtävien käsittelyä; yksi virheellinen ajastin ei pysäytä ohjelmaa.

## Fetch-koukut

Phasis sisältää oletusarvoisesti käytössä olevan toiminnon `fetch()`, jonka kuljetuskerroksena toimii PHP-ohjelman `ext-curl`. Kolmen koukun avulla upottaja voi vaihtaa kuljetustapaa, rajoittaa pyyntöjä tai ottaa evästeiden tallennuksen käyttöön. Kaikki kolme hyväksyvät joko PHP-kutsuttavan tai olion metodin; arvorakennetta koskevat vaatimukset ovat kohdassa [Web-rajapinnat](https://phasis.inline0.dev/fi/docs/compatibility/web-apis).

### setFetchTransport

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

Korvaa oletusarvoisen CurlTransport-kuljetuksen. Kutsuttava saa pyyntökuvauksen ja valinnaisen `AbortSignal` JsObject -olion; se palauttaa vastauskuvauksen.

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

Yleinen upotusmalli: asenna testeihin valekuljetus ja tuotantoon oikea curl-kuljetus.

### setFetchPolicy

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

Ennakkotarkistuksen käytäntökoukku. Saa arvon `Request` JsObject; voi heittää virheen pyyntöä estääkseen, palauttaa muokatun arvon `Request` pyynnön muuttamiseksi tai palauttaa arvon `null` tai olla palauttamatta mitään pyynnön sallimiseksi.

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

Valinnaisesti käyttöönotettava evästevarasto. Hyväksyy minkä tahansa olion, joka tarjoaa metodit `get(string $url): string` ja `set(string $url, string $header): void` — joko PHP-puolella tai JS-puolella.

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

Ilman arvoa `setCookieJar()` `fetch` ohittaa sekä saapuvat `Set-Cookie`-otsakkeet että lähtevät `Cookie`-otsakkeet — oletuksena evästeitä ei käytetä.

## Poikkeukset

| Luokka | Perii luokan | Milloin |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | eksplisiittinen JS `throw`, jota mikään ei siepannut |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | moottorin itse aiheuttama virhe (esimerkiksi `null.x` tai määrittelemätön muuttuja), jota mikään ei siepannut |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | silmukkaraja tai kutsupinon enimmäisraja ylittyi |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | edellä mainittujen luokkien kantaluokka |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | lähdekoodin jäsentäminen epäonnistuu |
| `\RuntimeException` | | `execFile()` ei pysty lukemaan tiedostoa; tapahtumasilmukka saavutti 100,000 syklin rajansa |

Skriptiä varten kannattaa siepata kaikki virheet sieppaamalla `Phasis\Exceptions\RuntimeError` ja `Phasis\Exceptions\SyntaxError` tai yksinkertaisesti `\RuntimeException`.

Omien PHP-isäntäfunktioidesi heittämät poikkeukset käsitellään eri tavalla: ne kulkevat tällä hetkellä rakenteiden JS `try` / `catch` läpi ja päätyvät muuttumattomina toiminnon `eval()` kutsujalle säilyttäen alkuperäisen luokkansa ([ongelma #20](https://github.com/inline0/phasis/issues/20)). Katso [Isäntäfunktiot](https://phasis.inline0.dev/fi/docs/interop/host-functions#throwing-back-into-js).

## Elinkaari

`Engine` on nopea rakentaa (kylmäkäynnistys kestää muutamia millisekunteja), ja vakiokirjasto asennetaan laiskasti. Useimmissa upotustilanteissa yksi pitkään käytössä oleva moottori prosessia kohden riittää. Usean vuokralaisen käytössä anna kullekin vuokralaiselle tai pyynnölle oma moottori, jotta globaalit muuttujat eivät vuoda niiden välillä. Uuden moottorin rakentaminen antaa tällä hetkellä luotettavimmin puhtaan aloitustilan (katso [nollaa](#reset)).

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