
# API

Die Ausführungs-API besteht aus einer Klasse: `Phasis\Engine`. Alles andere (`JsValue`, `JsFunction`, `JsObject`, …) ist intern und wird nur Host-Funktionen zugänglich gemacht, die zwischen PHP↔JS vermitteln. Die einzige weitere öffentliche Schnittstelle ist der eigenständige [Parser API](https://phasis.inline0.dev/de/docs/parser) (`Phasis\Parser\Parser` und die `Phasis\Ast`-Klassen), der Quellcode parst, ohne ihn auszuführen.

## Konstruktion

```php
use Phasis\Engine;

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

Eine neue `Engine` stellt die vollständige Standardbibliothek bereit – `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` und die grundlegenden Sprachintrinsics werden sofort installiert, damit die Spezifikation funktioniert (ohne `Function.prototype` gibt es keine Klassen, ohne `Promise` kein async/await und ohne `RegExp` keine Regex-Literale). Alles Weitere – `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` und die ArrayBuffer-Familie, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, die WeakRef-Familie, `Temporal`, `ShadowRealm`, das Web Platform Pack und das Fetch Pack – wird als Platzhalter-Accessor registriert und beim ersten Lesen angelegt. Das Lesen der globalen Variable ist transparent: `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')` funktionieren alle, und nach dem ersten Zugriff wird der Platzhalter durch einen regulären Daten-Descriptor ersetzt.

Übergib `eager: true`, wenn du ausdrücklich das Verhalten vor der verzögerten Initialisierung brauchst – alle integrierten Funktionen werden beim Erzeugen mit der vollständigen Descriptor-Oberfläche für die Introspektion installiert. Die Konformitätstestumgebung (`bin/test262`, `bin/wpt`) erstellt Engines im Eager-Modus, damit test262/WPT die erwarteten Descriptor-Formen sieht.

## eval

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

Parst `$source` als klassisches Skript im globalen Gültigkeitsbereich der Engine, führt es aus und lässt anschließend die Ereignisschleife laufen, bis sie leer ist. Der Wert des zuletzt ausgewerteten Ausdrucks wird in PHP konvertiert zurückgegeben (oder `null`, wenn das Programm keinen abschließenden Ausdruck hatte). Die Regeln findest du unter [Wertkonvertierung](https://phasis.inline0.dev/de/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)
```

Ein Promise wird nicht entpackt: `eval('Promise.resolve(5)')` gibt ein leeres Array zurück. Da die Ereignisschleife bis zur Rückgabe von `eval()` abgearbeitet wurde, speichere den aufgelösten Wert in einer globalen Variable und lies ihn mit einem zweiten Aufruf von `eval()` aus:

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

Ein explizites JS-`throw`, das von nichts abgefangen wird, erscheint als `Phasis\Exceptions\JsThrowable`; dessen Meldung lautet `"Name: message"` und seine Eigenschaft `$jsValue` enthält das ausgelöste `JsValue`. Vom Engine selbst ausgelöste Fehler, etwa beim Zugriff auf eine Eigenschaft von `null` oder bei einer undefinierten Variable, erscheinen stattdessen als `Phasis\Exceptions\TypeError`, `ReferenceError` oder `RangeError`. Alle diese Klassen erweitern `Phasis\Exceptions\RuntimeError`; Parse-Fehler erweitern `Phasis\Exceptions\SyntaxError`. Siehe [Ausnahmen](#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
```

Wertet `$source` als ES-Modul aus: `import` / `export`, Toplevel-`await` und `import.meta` stehen zur Verfügung, und relative Bezeichner werden relativ zu `$path` aufgelöst (ein absoluter Dateipfad). Die Ereignisschleife läuft wie bei `eval()`; zurückgegeben wird immer `null`. Lies Ergebnisse über globale Variablen oder den Konsolenpuffer aus.

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

Der Loader löst nur relative und absolute Pfade auf; siehe [Bekannte Einschränkungen](https://phasis.inline0.dev/de/docs/compatibility/limitations#module-loader).

## execFile

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

Liest eine Datei von der Festplatte und übergibt ihren Inhalt an `eval()`. Dadurch wird sie unabhängig von ihrer Dateiendung als klassisches Skript ausgeführt; eine Datei mit `import` / `export` schlägt mit einem SyntaxError fehl. Der tatsächliche Pfad der Datei dient als Basis für dynamische `import()`-Bezeichner. Verwende `evalAsModule()` für Moduldateien.

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

Kann die Datei nicht gelesen werden, gibt PHP eine Warnung aus und `execFile()` löst einen einfachen `\RuntimeException`-Fehler aus („Cannot read file: …“).

## setGlobal

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

Bindet eine globale Variable, die bei allen nachfolgenden JS-Auswertungen sichtbar ist. PHP-Werte werden in JS konvertiert:

| Typ von PHP | JS-Typ |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (auch wenn es eine Funktion bezeichnet, etwa `'strlen'`) |
| Array (Liste) | `Array` (Kopie) |
| Array (assoziativ) | `Object` (Kopie) |
| `Closure`, aufrufbares Objekt, `[$object, 'method']` | `function` |
| jedes andere Objekt, jeder Generator oder jede Ressource | `undefined` |

Arrays werden kopiert: Änderungen, die JS daran vornimmt, sind im PHP-Array nicht sichtbar. Einfache PHP-Objekte lassen sich derzeit nicht übergeben ([Issue #18](https://github.com/inline0/phasis/issues/18)); Workarounds findest du unter [Gemeinsam genutzte Objekte](https://phasis.inline0.dev/de/docs/interop/shared-objects).

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

Innerhalb von JS:

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

## call

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

Ruft eine globale JS-Funktion mit PHP Argumenten auf (konvertiert wie bei `setGlobal`). Der Rückgabewert wird zurück in PHP konvertiert. Der Aufruf eines nicht definierten Namens löst `Phasis\Exceptions\ReferenceError` aus; ist eine globale Variable keine Funktion, löst der Aufruf `Phasis\Exceptions\TypeError` aus.

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

## Konsolenausgabe

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

`console.log`, `console.info`, `console.warn`, `console.error` und `console.debug` geben nichts aus. Jeder Aufruf fügt dem Puffer der Engine eine formatierte Zeile hinzu. `getConsoleLines()` gibt die gepufferten Zeilen als Liste von Zeichenfolgen zurück, `getConsoleOutput()` gibt sie zusammengefügt mit `\n` zurück, und `clearConsole()` leert den Puffer. Zwischen Aufrufen von `eval()` wird der Puffer nicht geleert.

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

Legt ein zur Laufzeit anpassbares Ressourcenlimit fest. Es gibt genau eines:

| Limit | Standardwert | Beschreibung |
|---|---|---|
| `maxLoopIterations` | `100000` | maximale Iterationen pro Schleifendurchlauf von `for` / `while` / `do…while` im Tree-Walking-Interpreter |

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

Jeder andere Name wird stillschweigend ignoriert; es gibt keine Limits für Zeichenfolgenlänge, Konsolenausgabe oder Laufzeit.

Das Schleifenlimit wird noch nicht überall durchgesetzt. Schleifen, die auf der Bytecode-VM laufen, also Funktionsrümpfe und Programme auf oberster Ebene, die der Bytecode-Compiler akzeptiert, werden nie gezählt. Daher läuft `while (true) {}`, bis PHP es stoppt ([Issue #19](https://github.com/inline0/phasis/issues/19)). Verlass dich nicht darauf, damit nicht vertrauenswürdigen Code zu stoppen; nutze PHP's `max_execution_time` oder einen separaten Prozess.

Wird das Limit erreicht, kann ein JS `try` / `catch` den Fehler abfangen (als `Error` mit der Meldung "Maximum loop iterations exceeded"). Fängt ihn nichts ab, erreicht er PHP als `Phasis\Exceptions\InternalError`, das `Phasis\Exceptions\RuntimeError` erweitert, nicht `JsThrowable`.

Zwei weitere Obergrenzen sind fest in der Engine verankert und nicht zur Laufzeit anpassbar: `Phasis\Runtime\CallStack::maxDepth` (4,096 JS-Frames) und die Tiefenbegrenzung für transpilierte JsToPhp-Closures (8,192 PHP-Frames). Bei Überschreitung werfen beide "Maximum call stack size exceeded"; der Fehler erreicht PHP ebenfalls als `InternalError`. Sie liegen weit über dem Bedarf jedes legitimen JS-Programms — sie zu erhöhen erfordert eine Codeänderung, keine Konfiguration.

## reset

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

Löscht alle benutzerdefinierten globalen Variablen und den Konsolenpuffer und erstellt die Standardbibliothek sowie die Ereignisschleife neu.

Bei einer Engine, die im standardmäßigen Lazy-Modus erstellt wurde, verliert `reset()` derzeit alle verzögert installierten Built-ins: Danach sind `Map`, `JSON`, `Math`, `console`, `fetch` und alle weiteren nicht definiert ([Issue #22](https://github.com/inline0/phasis/issues/22)). Bis das behoben ist, erstelle stattdessen eine neue `Engine` oder erstelle sie mit `eager: true`; dort funktioniert `reset()`.

## Ereignisschleife

Die Engine enthält eine JavaScript-Ereignisschleife mit `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` und `queueMicrotask` auf dem globalen Objekt. Microtasks (Promise-Handler, `queueMicrotask`) werden zwischen jedem Macrotask abgearbeitet; Timer werden nach Fälligkeit über eine Task-Warteschlange pro Realm ausgelöst.

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

`eval()` und `evalAsModule()` rufen `runEventLoop()` automatisch auf, sobald der synchrone Code abgeschlossen ist. Ein Skript, das auf oberster Ebene `setTimeout(cb, 5000)` plant, führt daher `cb` aus, bevor `eval()` zurückkehrt. Die Schleife verwendet **virtuelle Zeit** und stellt ihre Uhr auf den nächsten anstehenden Zeitpunkt vor, statt PHP zu warten. Entspricht der Semantik von `node script.js`.

Für die Einbettung in Echtzeitanwendungen (lang laufende Server, UI-Integration, Planung anhand einer Echtzeituhr) führt `tickEventLoop()` eine Iteration aus (arbeitet Microtasks ab und löst fällige Timer aus) und kehrt zurück. `pendingTaskCount()` meldet ausstehende Timer; ein Embedder kann seine eigene Schleife steuern:

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

Eine außer Kontrolle geratene `setInterval` ist begrenzt: `runEventLoop()` wird nach 100,000 Zyklen abgebrochen und wirft ein einfaches `\RuntimeException` — der Fehler im JS-Code wird als klarer PHP-Fehler angezeigt, statt die Ausführung aufzuhängen. Das Intervall bleibt geplant, sodass jeder spätere Aufruf von `eval()` auf derselben Engine denselben Fehler wirft; verwirf die Engine danach. Nicht abgefangene Fehler in einem Timer-Callback werden über `error_log()` ausgegeben, und die Schleife arbeitet weiter. Ein fehlerhafter Timer beendet das Programm nicht.

## Fetch-Hooks

Phasis enthält standardmäßig aktivierte `fetch()`; als zugrunde liegender Transport dient `ext-curl` von PHP. Drei Hooks ermöglichen es einem Embedder, den Transport auszutauschen, Anfragen zu steuern oder die Cookie-Speicherung zu aktivieren. Alle drei akzeptieren entweder ein aufrufbares PHP oder eine Methode auf einem Objekt. Siehe [Web-APIs](https://phasis.inline0.dev/de/docs/compatibility/web-apis) für den Vertrag zur Wertestruktur.

### setFetchTransport

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

Ersetzt den standardmäßigen CurlTransport. Der Aufruf erhält einen Request-Deskriptor und ein optionales `AbortSignal` JsObject und gibt einen Response-Deskriptor zurück.

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

Ein gängiges Einbettungsmuster: In Tests wird ein Mock-Transport installiert, in der Produktion der echte Curl-Transport.

### setFetchPolicy

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

Hook für Richtlinienprüfungen vor der Anfrage. Erhält das `Request` JsObject; kann zum Ablehnen einen Fehler werfen, zum Umschreiben ein geändertes `Request` zurückgeben oder zum Zulassen `null`/keinen Wert zurückgeben.

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

Optionaler Cookie-Speicher. Akzeptiert jedes Objekt, das `get(string $url): string` und `set(string $url, string $header): void` bereitstellt — auf PHP- oder JS-Seite.

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

Ohne `setCookieJar()` ignoriert `fetch` sowohl eingehende `Set-Cookie`- als auch ausgehende `Cookie`-Header — standardmäßig werden **keine Cookies** verwendet.

## Ausnahmen

| Klasse | Erweitert | Wann |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | ein expliziter JS-`throw`, den nichts abgefangen hat |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | ein von der Engine selbst ausgelöster Fehler (zum Beispiel `null.x` oder eine nicht definierte Variable), den nichts abgefangen hat |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | das Schleifenlimit oder die Aufrufstapel-Obergrenze wurde überschritten |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | Basisklasse der obigen Klassen |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | der Quelltext lässt sich nicht parsen |
| `\RuntimeException` | | `execFile()` kann die Datei nicht lesen; die Ereignisschleife hat ihre Obergrenze von 100,000 Zyklen erreicht |

Um jeden Fehler eines Skripts abzufangen, fange `Phasis\Exceptions\RuntimeError` und `Phasis\Exceptions\SyntaxError` ab oder fange einfach `\RuntimeException` ab.

Ausnahmen, die von deinen eigenen PHP-Hostfunktionen ausgelöst werden, verhalten sich anders: Sie durchlaufen derzeit JS-`try` / `catch` und erreichen den Aufrufer von `eval()` unverändert und mit ihrer ursprünglichen Klasse ([Issue #20](https://github.com/inline0/phasis/issues/20)). Siehe [Hostfunktionen](https://phasis.inline0.dev/de/docs/interop/host-functions#throwing-back-into-js).

## Lebenszyklus

`Engine` lässt sich schnell erstellen (kalt in wenigen ms), und die Standardbibliothek wird verzögert installiert. Für die meisten Einbettungsszenarien reicht eine langlebige Engine pro Prozess. Bei Nutzung durch mehrere Mandanten solltest du jedem Mandanten oder jeder Anfrage eine eigene Engine geben, damit keine globalen Variablen zwischen ihnen durchsickern. Eine neue Engine zu erstellen, ist derzeit der verlässlichste Neustart (siehe [reset](#reset)).

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