
# API

Körningsytan API består av en enda klass: `Phasis\Engine`. Allt annat (`JsValue`, `JsFunction`, `JsObject`, …) är internt i motorn och exponeras bara för värdfunktioner som överbryggar PHP↔JS. Den enda andra publika ytan är den fristående parsern [API](https://phasis.inline0.dev/sv/docs/parser) (`Phasis\Parser\Parser` och `Phasis\Ast`-klasserna), som tolkar källkod utan att köra den.

## Konstruktion

```php
use Phasis\Engine;

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

En ny `Engine` exponerar hela standardbiblioteket — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` och språkets grundläggande inbyggda funktioner installeras omedelbart så att specifikationen fungerar (du kan inte ha klasser utan `Function.prototype`, async/await utan `Promise` eller regexliteraler utan `RegExp`). Allt annat — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` och array-buffer-familjen, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, weak-ref-familjen, `Temporal`, `ShadowRealm`, Web Platform Pack och Fetch Pack — registreras som en platshållaraccessor och materialiseras vid första läsningen. Läsning av globala objektet är transparent: `Map`, `typeof Map`, `'Map' in globalThis` och `Object.keys(globalThis).includes('Map')` fungerar alla, och efter första läsningen ersätts platshållaren med en vanlig datadeskriptor.

Ange `eager: true` om du specifikt behöver beteendet före lazy-installationen — alla inbyggda funktioner installeras vid konstruktionen och hela deskriptorgränssnittet är synligt vid introspektion. Konformitetstestriggen (`bin/test262`, `bin/wpt`) konstruerar motorer i eager-läge så att test262/WPT ser de deskriptorformer de förväntar sig.

## eval

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

Tolkar och kör `$source` som ett klassiskt skript i motorns globala omfång och kör sedan händelseloopen tills den är tom. Returnerar värdet från det senast utvärderade uttrycket konverterat till PHP (eller `null` om programmet saknade ett avslutande uttryck). Se [Konvertering av värden](https://phasis.inline0.dev/sv/docs/interop/values) för reglerna.

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

Ett Promise packas inte upp: `eval('Promise.resolve(5)')` returnerar en tom array. Eftersom händelseloopen har tömts när `eval()` returnerar lagrar du det lösta värdet i en global variabel och läser det med ett andra anrop till `eval()`:

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

Ett uttryckligt JS `throw` som ingen fångar upp visas som `Phasis\Exceptions\JsThrowable`, vars meddelande är `"Name: message"` och vars egenskap `$jsValue` innehåller det kastade `JsValue`. Fel som motorn själv orsakar, till exempel vid läsning av en egenskap hos `null` eller en odefinierad variabel, visas i stället som `Phasis\Exceptions\TypeError`, `ReferenceError` eller `RangeError`. Alla dessa utökar `Phasis\Exceptions\RuntimeError`; tolkningsfel utökar `Phasis\Exceptions\SyntaxError`. Se [Undantag](#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
```

Utvärderar `$source` som en ES-modul: `import` / `export`, toppnivå-`await` och `import.meta` är tillgängliga, och relativa specifikatorer löses mot `$path` (en absolut filsökväg). Kör händelseloopen som `eval()` och returnerar alltid `null`; läs resultat via globala variabler eller konsolbufferten.

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

Laddaren löser bara relativa och absoluta sökvägar; se [Kända begränsningar](https://phasis.inline0.dev/sv/docs/compatibility/limitations#module-loader).

## execFile

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

Läser en fil från disken och skickar innehållet till `eval()`, så den körs som ett klassiskt skript oavsett filändelse. En fil med `import` / `export` ger ett SyntaxError. Filens verkliga sökväg blir basen för dynamiska `import()`-specifikatorer. Använd `evalAsModule()` för modulfiler.

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

Om filen inte kan läsas avger PHP en varning och `execFile()` kastar ett vanligt `\RuntimeException` ("Kan inte läsa filen: …").

## setGlobal

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

Binder en global variabel som är synlig för alla efterföljande JS-utvärderingar. PHP-värden konverteras till JS:

| PHP-typ | JS-typ |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (även när det är namnet på en funktion, till exempel `'strlen'`) |
| array (lista) | `Array` (en kopia) |
| array (associativ) | `Object` (en kopia) |
| `Closure`, anropbart objekt, `[$object, 'method']` | `function` |
| annat objekt, generator eller resurs | `undefined` |

Arrayer kopieras: ändringar som JS gör i dem syns inte i PHP-arrayen. Vanliga PHP-objekt kan ännu inte skickas ([problem #18](https://github.com/inline0/phasis/issues/18)); se [Delade objekt](https://phasis.inline0.dev/sv/docs/interop/shared-objects) för lösningar.

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

Inuti JS:

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

## call

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

Anropar en global JS-funktion med PHP-argument (konverterade som för `setGlobal`). Returvärdet konverteras tillbaka till PHP. Anrop med ett odefinierat namn kastar `Phasis\Exceptions\ReferenceError`; anrop av en global variabel som inte är en funktion kastar `Phasis\Exceptions\TypeError`.

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

## Konsolutdata

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

`console.log`, `console.info`, `console.warn`, `console.error` och `console.debug` skriver inte ut något. Varje anrop lägger till en formaterad rad i en buffert i motorn. `getConsoleLines()` returnerar de buffrade raderna som en lista med strängar, `getConsoleOutput()` returnerar dem sammanfogade med `\n` och `clearConsole()` tömmer bufferten. Bufferten rensas inte mellan anrop till `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
```

Anger en resursgräns som kan ändras under körning. Det finns exakt en:

| Gräns | Standardvärde | Beskrivning |
|---|---|---|
| `maxLoopIterations` | `100000` | maximalt antal iterationer per `for`- / `while`- / `do…while`-loopkörning i den trädvandrade tolken |

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

Alla andra namn ignoreras i tysthet. Det finns inga gränser för stränglängd, konsolutdata eller körtid i realtid.

Loopgränsen tillämpas ännu inte överallt. Loopar som körs på bytekodens VM, vilket omfattar funktionskroppar och program på toppnivå som bytekodskompilatorn godtar, räknas aldrig. Därför kör `while (true) {}` tills PHP stoppar den ([problem #19](https://github.com/inline0/phasis/issues/19)). Förlita dig inte på den för att stoppa opålitlig kod; använd PHPs `max_execution_time` eller en separat process.

När gränsen nås kan felet fångas av en JS `try` / `catch` (som ett `Error` med meddelandet "Maximum loop iterations exceeded"). Om ingen fångar upp det når det PHP som `Phasis\Exceptions\InternalError`, vilket utökar `Phasis\Exceptions\RuntimeError`, inte `JsThrowable`.

Två ytterligare tak är inbyggda i motorn och kan inte ändras under körning: `Phasis\Runtime\CallStack::maxDepth` (4,096 JS ramar) och djupvakten för transpilerade JsToPhp-closures (8,192 PHP ramar). Om något av taken överskrids kastas "Maximum call stack size exceeded", som också når PHP som `InternalError`. Gränserna ligger långt över vad legitima JS-program behöver — att höja dem kräver en kodändring, inte en konfigurationsinställning.

## reset

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

Rensar alla användardefinierade globala variabler och konsolbufferten samt bygger om standardbiblioteket och händelseloopen.

I en motor som skapats i standardläget lazy förlorar `reset()` för närvarande alla inbyggda funktioner som installerats lazy. Efteråt är `Map`, `JSON`, `Math`, `console`, `fetch` och resten odefinierade ([problem #22](https://github.com/inline0/phasis/issues/22)). Tills det är åtgärdat bör du i stället skapa en ny `Engine` eller skapa den med `eager: true`, där `reset()` fungerar.

## Händelseloop

Motorn levereras med en JavaScript-händelseloop med `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` och `queueMicrotask` på det globala objektet. Mikrotasks (Promise-hanterare, `queueMicrotask`) töms mellan varje makrotask. Timers utlöses i ordning efter deadline via en uppgiftskö per realm.

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

`eval()` och `evalAsModule()` anropar automatiskt `runEventLoop()` när den synkrona koden har slutförts. Ett skript som schemalägger `setTimeout(cb, 5000)` på toppnivå kör därför `cb` innan `eval()` returnerar — loopen använder **virtuell tid** och flyttar klockan fram till nästa väntande deadline i stället för att vänta PHP. Följer semantiken i `node script.js`.

För inbäddning i realtid (servrar som kör länge, UI-integration och schemaläggning mot en verklig klocka) kör `tickEventLoop()` en iteration (tömmer mikrotasks och utlöser timers som förfallit) och returnerar. `pendingTaskCount()` rapporterar väntande timers. Den som bäddar in motorn kan driva sin egen pump:

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

En skenande `setInterval` begränsas: `runEventLoop()` sätter ett tak på 100,000 cykler och kastar ett vanligt `\RuntimeException` — felet i JS-koden visas som ett tydligt PHP i stället för att orsaka ett låst läge. Intervallet fortsätter att vara schemalagt, så varje senare `eval()` på samma motor kastar samma fel. Kassera motorn efteråt. Ohanterade fel i en timer-callback skickas via `error_log()` och loopen fortsätter att tömma kön. En timer med fel stoppar inte programmet.

## Fetch-hooks

Phasis levereras med `fetch()` aktiverat som standard och PHPs `ext-curl` som underliggande transport. Tre hooks låter den som bäddar in motorn byta transport, begränsa förfrågningar eller aktivera lagring av cookies. Alla tre godtar antingen en anropsbar PHP eller en metod på ett objekt. Se [Webb-API:er](https://phasis.inline0.dev/sv/docs/compatibility/web-apis) för reglerna för värdeformer.

### setFetchTransport

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

Ersätter standardtransporten CurlTransport. Den anropsbara funktionen tar emot en förfrågningsbeskrivning och ett valfritt `AbortSignal` JsObject och returnerar en svarsbeskrivning.

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

Ett vanligt mönster vid inbäddning: installera en mocktransport i tester och den riktiga curl-transporten i produktion.

### setFetchPolicy

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

Policy-hook som körs före förfrågan. Tar emot `Request` JsObject; kan kasta ett undantag för att neka, returnera ett ändrat `Request` för att skriva om eller returnera `null`/inget värde för att tillåta.

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

Valfri cookie-lagring. Godtar alla objekt som exponerar `get(string $url): string` och `set(string $url, string $header): void` — på PHP-sidan eller JS-sidan.

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

Utan `setCookieJar()` ignorerar `fetch` både inkommande `Set-Cookie`- och utgående `Cookie`-headers — standardinställningen är **inga cookies**.

## Undantag

| Klass | Utökar | När |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | ett uttryckligt JS `throw` som ingen fångade upp |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | ett fel som motorn själv orsakat (till exempel `null.x` eller en odefinierad variabel) och som ingen fångade upp |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | loopgränsen eller taket för anropsstacken överskreds |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | basklass för klasserna ovan |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | källkoden kan inte tolkas |
| `\RuntimeException` | | `execFile()` kan inte läsa filen; händelseloopen nådde sitt tak på 100,000 cykler |

För att fånga alla fel i ett skript, fånga både `Phasis\Exceptions\RuntimeError` och `Phasis\Exceptions\SyntaxError`, eller använd bara `\RuntimeException`.

Undantag som kastas av dina egna värdfunktioner i PHP skiljer sig åt: de passerar för närvarande genom JS `try` / `catch` och når anroparen av `eval()` oförändrade, med sin ursprungliga klass ([problem #20](https://github.com/inline0/phasis/issues/20)). Se [Värdfunktioner](https://phasis.inline0.dev/sv/docs/interop/host-functions#throwing-back-into-js).

## Livscykel

`Engine` är snabb att konstruera (~några ms kallstart) och standardbiblioteket installeras lazy. För de flesta inbäddningsscenarier fungerar en enda långlivad motor per process bra. Vid användning för flera klienter bör varje klient eller förfrågan få en egen motor så att globala variabler aldrig läcker mellan dem. Att konstruera en ny motor är det tillförlitliga sättet att börja om rent i dag (se [reset](#reset)).

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