
# API

De uitvoeringsAPI is één klasse: `Phasis\Engine`. Al het overige (`JsValue`, `JsFunction`, `JsObject`, …) is intern voor de engine en wordt alleen beschikbaar gemaakt voor hostfuncties die PHP↔JS overbruggen. Het enige andere openbare oppervlak is de zelfstandige [parserAPI](https://phasis.inline0.dev/nl/docs/parser) (`Phasis\Parser\Parser` en de `Phasis\Ast`-klassen), die broncode parseert zonder die uit te voeren.

## Constructie

```php
use Phasis\Engine;

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

Een nieuwe `Engine` stelt de volledige standaardbibliotheek beschikbaar: `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` en de kernintrinsics van de taal worden direct geïnstalleerd, zodat de specificatie werkt (je kunt geen classes hebben zonder `Function.prototype`, geen async/await zonder `Promise` en geen regex-literals zonder `RegExp`). Al het overige — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` en de array-bufferfamilie, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, de weak-ref-familie, `Temporal`, `ShadowRealm`, het Web Platform Pack en het Fetch Pack — wordt geregistreerd als een accessor met een plaatshouder en wordt aangemaakt bij de eerste uitlezing. Het uitlezen van de globale variabele verloopt transparant: `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')` werken allemaal, en na de eerste uitlezing wordt de plaatshouder vervangen door een gewone data descriptor.

Geef `eager: true` mee als je specifiek het gedrag van vóór lazy loading nodig hebt: elke ingebouwde functie wordt bij de constructie geïnstalleerd en de volledige descriptor is zichtbaar bij introspectie. De conformiteitsharness (`bin/test262`, `bin/wpt`) maakt engines aan in eager mode, zodat test262/WPT de verwachte descriptorvormen zien.

## eval

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

Parseert `$source` en voert het uit als een klassiek script in de globale scope van de engine; daarna wordt de event loop uitgevoerd totdat die leeg is. De waarde van de laatst geëvalueerde expressie wordt teruggegeven, omgezet naar PHP (of `null` als het programma geen afsluitende expressie had). Zie [Waarden omzetten](https://phasis.inline0.dev/nl/docs/interop/values) voor de regels.

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

Een Promise wordt niet uitgepakt: `eval('Promise.resolve(5)')` geeft een lege array terug. Omdat de event loop is leeggemaakt wanneer `eval()` terugkeert, sla je de afgehandelde waarde op in een globale variabele en lees je die uit met een tweede aanroep van `eval()`:

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

Een expliciete JS-`throw` die nergens wordt opgevangen, verschijnt als `Phasis\Exceptions\JsThrowable`, met als bericht `"Name: message"` en met de gegooide `JsValue` in de property `$jsValue`. Fouten die de engine zelf genereert, bijvoorbeeld bij het uitlezen van een property van `null` of van een niet-gedefinieerde variabele, verschijnen als `Phasis\Exceptions\TypeError`, `ReferenceError` of `RangeError`. Deze zijn allemaal afgeleid van `Phasis\Exceptions\RuntimeError`; parsefouten van `Phasis\Exceptions\SyntaxError`. Zie [Uitzonderingen](#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
```

Evalueert `$source` als een ES-module: `import` / `export`, top-level `await` en `import.meta` zijn beschikbaar en relatieve specifiers worden opgelost ten opzichte van `$path` (een absoluut bestandspad). De event loop wordt net als bij `eval()` uitgevoerd en er wordt altijd `null` teruggegeven; lees resultaten via globale variabelen of de consolebuffer.

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

De loader lost alleen relatieve en absolute paden op; zie [Bekende beperkingen](https://phasis.inline0.dev/nl/docs/compatibility/limitations#module-loader).

## execFile

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

Leest een bestand van schijf en geeft de inhoud door aan `eval()`, zodat die als klassiek script wordt uitgevoerd, ongeacht de extensie. Een bestand met `import` / `export` veroorzaakt een SyntaxError. Het werkelijke pad van het bestand wordt de basis voor dynamische specifiers van `import()`. Gebruik `evalAsModule()` voor modulebestanden.

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

Als het bestand niet kan worden gelezen, geeft PHP een waarschuwing en gooit `execFile()` een gewone `\RuntimeException` ("Cannot read file: …").

## setGlobal

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

Koppelt een globale variabele die zichtbaar is voor alle volgende evaluaties van JS. Waarden van PHP worden omgezet naar JS:

| Type PHP | Type JS |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (ook als het een functie bij naam noemt, zoals `'strlen'`) |
| array (lijst) | `Array` (een kopie) |
| array (associatief) | `Object` (een kopie) |
| `Closure`, aanroepbaar object, `[$object, 'method']` | `function` |
| elk ander object, generator of resource | `undefined` |

Arrays worden gekopieerd: wijzigingen die JS erin aanbrengt, zijn niet zichtbaar in de array van PHP. Gewone objecten van PHP kunnen nog niet worden doorgegeven ([issue #18](https://github.com/inline0/phasis/issues/18)); zie [Gedeelde objecten](https://phasis.inline0.dev/nl/docs/interop/shared-objects) voor oplossingen.

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

Binnen JS:

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

## call

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

Roept een globale functie van JS aan met PHP-argumenten (omgezet zoals bij `setGlobal`). De geretourneerde waarde wordt terug omgezet naar PHP. Een naam aanroepen die niet is gedefinieerd, veroorzaakt `Phasis\Exceptions\ReferenceError`; een globale variabele aanroepen die geen functie is, veroorzaakt `Phasis\Exceptions\TypeError`.

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

## Console-uitvoer

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

`console.log`, `console.info`, `console.warn`, `console.error` en `console.debug` worden niet afgedrukt. Elke aanroep voegt één opgemaakte regel toe aan een buffer in de engine. `getConsoleLines()` geeft de regels in de buffer als een lijst met tekenreeksen terug, `getConsoleOutput()` voegt ze samen met `\n` ertussen en `clearConsole()` leegt de buffer. De buffer wordt niet gewist tussen aanroepen van `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
```

Stelt een tijdens runtime aanpasbare resourcelimiet in. Er is er precies één:

| Limiet | Standaard | Beschrijving |
|---|---|---|
| `maxLoopIterations` | `100000` | maximaal aantal iteraties per uitvoering van een `for` / `while` / `do…while`-lus door de tree-walking-interpreter |

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

Elke andere naam wordt stilzwijgend genegeerd; er zijn geen limieten voor tekenreekslengte, console-uitvoer of wandkloktijd.

De luslimiet wordt nog niet overal afgedwongen. Lussen die op de bytecode-VM draaien, waaronder functie-bodies en topprogramma's die de bytecodecompiler accepteert, worden nooit geteld. Daarom draait `while (true) {}` door totdat PHP het stopt ([issue #19](https://github.com/inline0/phasis/issues/19)). Vertrouw er niet op om niet-vertrouwde code te stoppen; gebruik de `max_execution_time` van PHP of een afzonderlijk proces.

Wanneer de limiet wordt bereikt, kan de fout worden opgevangen door een JS-`try` / `catch` (als een `Error` met het bericht "Maximum loop iterations exceeded"). Als niets de fout opvangt, bereikt die PHP als `Phasis\Exceptions\InternalError`, dat is afgeleid van `Phasis\Exceptions\RuntimeError`, niet van `JsThrowable`.

Twee extra bovengrenzen zijn in de engine ingebouwd en kunnen niet tijdens runtime worden aangepast: `Phasis\Runtime\CallStack::maxDepth` (4,096 frames van JS) en de dieptebewaking voor JsToPhp-transpiled closures (8,192 frames van PHP). Bij overschrijding gooien beide "Maximum call stack size exceeded"; ook die fout bereikt PHP als `InternalError`. De grenzen liggen ruim boven wat een legitiem JS-programma nodig heeft; verhogen vereist een codewijziging en kan niet via configuratie.

## reset

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

Wist alle door de gebruiker gedefinieerde globale variabelen en de consolebuffer, en bouwt de standaardbibliotheek en de event loop opnieuw op.

Bij een engine die in de standaardmodus met lazy loading is gemaakt, verliest `reset()` momenteel alle ingebouwde functies die lazy zijn geïnstalleerd. Daarna zijn `Map`, `JSON`, `Math`, `console`, `fetch` en de rest niet gedefinieerd ([issue #22](https://github.com/inline0/phasis/issues/22)). Maak tot dit is opgelost liever een nieuwe `Engine` aan, of maak er een aan met `eager: true`; dan werkt `reset()` wel.

## Eventloop

De engine wordt geleverd met een JavaScript-eventloop met `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` en `queueMicrotask` op het globale object. Microtasks (Promise-handlers, `queueMicrotask`) worden tussen elke macrotask afgehandeld; timers worden op deadlinevolgorde uitgevoerd via een taakwachtrij per realm.

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

`eval()` en `evalAsModule()` roepen `runEventLoop()` automatisch aan nadat de synchrone code is voltooid. Een script dat op topniveau `setTimeout(cb, 5000)` plant, wordt dus uitgevoerd `cb` voordat `eval()` terugkeert — de eventloop gebruikt **virtuele tijd** en verzet de klok naar de volgende openstaande deadline in plaats van PHP te wachten. Komt overeen met de semantiek van `node script.js`.

Voor realtime-inbedding (langlopende servers, integratie met UI, planning op basis van een echte klok) voert `tickEventLoop()` één iteratie uit (microtasks afhandelen, eventuele timers uitvoeren) en keert terug. `pendingTaskCount()` meldt openstaande timers; een embedder kan zijn eigen eventloop aansturen:

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

Een op hol geslagen `setInterval` wordt begrensd: `runEventLoop()` stopt na 100,000 cycli en gooit een gewone `\RuntimeException` — de fout in de code van JS wordt zichtbaar als een duidelijke PHP-fout in plaats van als een vastloper. Het interval blijft ingepland, dus elke latere `eval()` op dezelfde engine gooit dezelfde fout; werp de engine daarna weg. Niet-opgevangen fouten in een timercallback worden via `error_log()` doorgegeven en de eventloop blijft taken afhandelen; één slechte timer stopt het programma niet.

## Fetch-hooks

Phasis wordt standaard geleverd met `fetch()` ingeschakeld en gebruikt PHP's `ext-curl` als onderliggend transport. Met drie hooks kan een embedder het transport vervangen, verzoeken filteren of cookieopslag inschakelen. Alle drie accepteren een callable van het type PHP of een methode op een object; zie [Web-API's](https://phasis.inline0.dev/nl/docs/compatibility/web-apis) voor de contractvorm van de waarden.

### setFetchTransport

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

Vervang de standaard CurlTransport. De callable ontvangt een verzoekdescriptor en een optioneel `AbortSignal` JsObject; en retourneert een responsdescriptor.

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

Een veelgebruikte manier om dit in te bedden: installeer een mocktransport in tests en het echte curl-transport in productie.

### setFetchPolicy

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

Beleids-hook vóór verzending. Ontvangt de `Request` JsObject; kan een fout gooien om te weigeren, een aangepaste `Request` retourneren om het verzoek te herschrijven, of `null`/geen waarde retourneren om toe te staan.

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

Optionele cookieopslag. Accepteert elk object dat `get(string $url): string` en `set(string $url, string $header): void` beschikbaar stelt — aan de PHP- of JS-kant.

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

Zonder `setCookieJar()` negeert `fetch` zowel inkomende `Set-Cookie`- als uitgaande `Cookie`-headers — standaard zijn er **geen cookies**.

## Uitzonderingen

| Klasse | Uitbreiding van | Wanneer |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | een expliciete JS `throw` die niet is opgevangen |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | een fout die de engine zelf heeft veroorzaakt (bijvoorbeeld `null.x` of een ongedefinieerde variabele) en die niet is opgevangen |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | de looplimiet of de maximale aanroepstack is overschreden |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | basisklasse van bovenstaande klassen |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | de broncode kan niet worden geparseerd |
| `\RuntimeException` | | `execFile()` kan het bestand niet lezen; de eventloop heeft de limiet van 100,000 cycli bereikt |

Vang elke fout van een script op met `Phasis\Exceptions\RuntimeError` en `Phasis\Exceptions\SyntaxError`, of gebruik eenvoudigweg `\RuntimeException`.

Uitzonderingen die door je eigen PHP-hostfuncties worden gegooid, werken anders: ze gaan momenteel ongewijzigd door JS `try` / `catch` en bereiken de aanroeper van `eval()` met hun oorspronkelijke klasse ([issue #20](https://github.com/inline0/phasis/issues/20)). Zie [Hostfuncties](https://phasis.inline0.dev/nl/docs/interop/host-functions#throwing-back-into-js).

## Levenscyclus

`Engine` is snel te construeren (~enkele ms bij een koude start) en de standaardbibliotheek wordt pas geladen wanneer die nodig is. Voor de meeste inbeddingsscenario's volstaat één langlevende engine per proces. Geef bij gebruik met meerdere tenants elke tenant of elk verzoek een eigen engine, zodat globale variabelen niet tussen tenants lekken. Een nieuwe engine maken is momenteel de betrouwbare manier om met een schone lei te beginnen (zie [reset](#reset)).

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