
# API

A végrehajtási API egyetlen osztály: `Phasis\Engine`. Minden más (`JsValue`, `JsFunction`, `JsObject`, …) a motor belső része, és csak a PHP↔JS közötti kapcsolatot biztosító host-függvények számára érhető el. Az egyetlen másik nyilvános felület az önálló [parser API](https://phasis.inline0.dev/hu/docs/parser) (`Phasis\Parser\Parser` és a `Phasis\Ast` osztályok), amely végrehajtás nélkül elemzi a forrást.

## Létrehozás

```php
use Phasis\Engine;

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

Egy új `Engine` a teljes standard könyvtárat teszi elérhetővé — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` és a nyelv alapvető beépített elemei azonnal települnek, hogy működjön a specifikáció (osztályok nem létezhetnek `Function.prototype` nélkül, az async/await az `Promise` nélkül, a reguláris kifejezések literáljai pedig az `RegExp` nélkül). Minden más — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` és a tömbpuffer-család, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, a gyenge hivatkozások családja, `Temporal`, `ShadowRealm`, a Web Platform Pack és a Fetch Pack — helyőrző hozzáférőként van regisztrálva, és az első olvasáskor jön létre. A globális változó olvasása átlátható: a `Map`, `typeof Map`, `'Map' in globalThis` és `Object.keys(globalThis).includes('Map')` mind működik, az első olvasás után pedig a helyőrzőt normál adatleíró váltja fel.

Adja meg a `eager: true` értéket, ha kifejezetten az előzetes lusta betöltéses viselkedésre van szüksége — azaz minden beépített elem már a létrehozáskor települ, és a teljes leírófelület látható az introspekció számára. A megfelelőségi tesztfuttató (`bin/test262`, `bin/wpt`) előre betöltött módban hozza létre a motorokat, hogy a test262/WPT a várt leíróalakokat lássa.

## eval

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

A `$source` elemet klasszikus parancsfájlként elemzi és futtatja a motor globális hatókörében, majd addig futtatja az eseményhurkot, amíg ki nem ürül. Az utolsó kiértékelt kifejezés értékét PHP formátumban adja vissza (vagy `null` értéket, ha a program végén nem volt kifejezés). A szabályokat lásd az [Értékátalakítás](https://phasis.inline0.dev/hu/docs/interop/values) oldalon.

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

A Promise nincs feloldva: a `eval('Promise.resolve(5)')` üres tömböt ad vissza. Mivel az eseményhurok leáll, mire a `eval()` visszatér, tárolja az elszámolt értéket egy globális változóban, majd olvassa ki egy második `eval()` hívással:

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

A kifejezett JS `throw`, amelyet semmi sem kap el, `Phasis\Exceptions\JsThrowable` formájában jelenik meg; az üzenete `"Name: message"`, a `$jsValue` tulajdonsága pedig a kiváltott `JsValue` értéket tartalmazza. A motor által maga előidézett hibák, például egy `null` tulajdonságának olvasása vagy egy definiálatlan változó használata ehelyett `Phasis\Exceptions\TypeError`, `ReferenceError` vagy `RangeError` formájában jelennek meg. Ezek mindegyike a `Phasis\Exceptions\RuntimeError` osztályból származik; az elemzési hibák a `Phasis\Exceptions\SyntaxError` osztályból. Lásd: [Kivételek](#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
```

A `$source` értéket ES-modulként értékeli ki: a `import` / `export`, a felső szintű `await` és `import.meta` elérhető, a relatív specifikátorok pedig a `$path` alapján oldódnak fel (abszolút fájlútvonal). Az eseményhurkot az `eval()` módjára futtatja, és mindig `null` értéket ad vissza; az eredményeket globális változókon vagy a konzolpufferen keresztül olvashatja ki.

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

A betöltő csak a relatív és abszolút útvonalakat oldja fel; lásd: [Ismert korlátozások](https://phasis.inline0.dev/hu/docs/compatibility/limitations#module-loader).

## execFile

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

Beolvassa a fájlt a lemezről, és tartalmát átadja a `eval()` számára, így a kiterjesztéstől függetlenül klasszikus parancsfájlként fut le; a `import` / `export` elemet tartalmazó fájl SyntaxError hibát okoz. A fájl valódi elérési útja lesz a dinamikus `import()` specifikátorok alapja. Modulfájlokhoz használja a `evalAsModule()` elemet.

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

Ha a fájl nem olvasható, a PHP figyelmeztetést jelenít meg, a `execFile()` pedig egyszerű `\RuntimeException` hibát dob ("Cannot read file: …").

## setGlobal

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

Globális változót köt be, amely az összes későbbi JS kiértékelésben látható. A PHP értékek JS formátumra alakulnak:

| PHP típusa | JS típusa |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (akkor is, ha függvény neve, például `'strlen'`) |
| tömb (lista) | `Array` (másolat) |
| tömb (asszociatív) | `Object` (másolat) |
| `Closure`, hívható objektum, `[$object, 'method']` | `function` |
| bármely más objektum, generátor vagy erőforrás | `undefined` |

A tömbökről másolat készül: a JS által végrehajtott módosítások nem láthatók a PHP tömbben. Egyszerű PHP objektumokat még nem lehet átadni ([18. számú hiba](https://github.com/inline0/phasis/issues/18)); a kerülőmegoldásokért lásd: [Megosztott objektumok](https://phasis.inline0.dev/hu/docs/interop/shared-objects).

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

A JS belsejében:

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

## call

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

Egy globális JS függvényt hív meg PHP argumentummal (az `setGlobal` szerinti átalakítással). A visszatérési érték PHP formátumra alakul. Ha olyan nevet hív meg, amely nincs definiálva, `Phasis\Exceptions\ReferenceError` hiba keletkezik; ha egy nem függvény globális változót hív meg, `Phasis\Exceptions\TypeError` hiba keletkezik.

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

## Konzolkimenet

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

A `console.log`, `console.info`, `console.warn`, `console.error` és `console.debug` nem ír ki semmit. Minden hívás egy formázott sort fűz a motor pufferéhez. A `getConsoleLines()` listaként adja vissza a puffer sorait, a `getConsoleOutput()` a `\n` elválasztóval összefűzve adja vissza őket, a `clearConsole()` pedig kiüríti a puffert. A puffer nem ürül ki az egyes `eval()` hívások között.

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

Futásidőben módosítható erőforráskorlátot állít be. Pontosan egy ilyen korlát van:

| Korlát | Alapértelmezett érték | Leírás |
|---|---|---|
| `maxLoopIterations` | `100000` | a fa-bejárásos értelmező által futtatott `for` / `while` / `do…while` ciklusok maximális iterációszáma |

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

Minden más nevet csendben figyelmen kívül hagy; nincs korlátozás a karakterlánc hosszára, a konzolkimenetre vagy a futásidőre.

A cikluskorlátot még nem mindenhol kényszeríti ki. A bytecode VM-en futó ciklusokat — amelyek a függvénytörzseket és a bytecode-fordító által elfogadott felső szintű programokat fedik le — nem számolja, ezért a `while (true) {}` addig fut, amíg a PHP le nem állítja ([19. számú hiba](https://github.com/inline0/phasis/issues/19)). Ne hagyatkozzon erre nem megbízható kód leállításához; használja a PHP `max_execution_time` értékét vagy külön folyamatot.

A korlát elérésekor a hibát elkaphatja egy JS `try` / `catch` (`Error` néven, a "Maximum loop iterations exceeded" üzenettel). Ha semmi nem kapja el, PHP formájában jut el a `Phasis\Exceptions\InternalError` elemhez; ez a `Phasis\Exceptions\RuntimeError` osztályból származik, nem a `JsThrowable` osztályból.

A motorba két további, beépített és futásidőben nem állítható felső korlát került: `Phasis\Runtime\CallStack::maxDepth` (4,096 JS keret) és a JsToPhp által transzpilált lezárások mélységkorlátja (8,192 PHP keret). Túllépésükkor mindkettő „Maximum call stack size exceeded” hibát dob, amely PHP-ként, `InternalError` formájában is megjelenik. Jóval meghaladják a legitim JS programok igényeit — növelésük kódmódosítást igényel, nem konfigurációs beállítás.

## Visszaállítás

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

Törli az összes felhasználó által definiált globális változót és a konzol pufferét, majd újraépíti a standard könyvtárat és az eseményhurkot.

Az alapértelmezett lusta módban létrehozott motorban a `reset()` jelenleg elveszíti az összes lustán telepített beépített elemet: utána `Map`, `JSON`, `Math`, `console`, `fetch` és a többi is definiálatlan lesz ([issue #22](https://github.com/inline0/phasis/issues/22)). Amíg ezt nem javítják, inkább hozzon létre egy új `Engine` objektumot, vagy használja a `eager: true` beállítást, ahol a `reset()` működik.

## Eseményhurok

A motor egy JavaScript eseményhurkot biztosít, amelyben a globális objektumon elérhető a `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` és a `queueMicrotask`. A mikrof feladatok (Promise-kezelők, `queueMicrotask`) minden makrof feladat között lefutnak; az időzítők határidő szerinti sorrendben aktiválódnak, realm-enkénti feladatsoron keresztül.

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

A `eval()` és a `evalAsModule()` automatikusan meghívja a `runEventLoop()` függvényt a szinkron kód befejeződése után. Ezért egy legfelső szinten `setTimeout(cb, 5000)` ütemező szkriptben a `cb` lefut, mielőtt a `eval()` visszatérne — a hurok **virtuális időt** használ: az alvás PHP helyett a következő függő határidőre állítja az órát. A `node script.js` szemantikáját követi.

Valós idejű beágyazás esetén (hosszan futó szerverek, UI-integráció, valós órához igazított ütemezés) a `tickEventLoop()` egyetlen iterációt futtat (kiüríti a mikrof feladatokat, aktiválja az esedékes időzítőket), majd visszatér. A `pendingTaskCount()` jelzi a még függő időzítőket; a beágyazó saját ciklust futtathat:

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

Egy elszabadult `setInterval` korlátozott: a `runEventLoop()` legfeljebb 100,000 ciklust futtat, majd egyszerű `\RuntimeException` hibát dob — az JS kód hibája egyértelmű PHP hibaként jelenik meg, nem a program lefagyásaként. Az időköz ütemezve marad, ezért ugyanazon a motoron minden későbbi `eval()` ugyanazt a hibát dobja; utána dobja el a motort. Az időzítő visszahívásában kezeletlen hibák a `error_log()` útján kerülnek kibocsátásra, az eseményhurok pedig folytatja a feladatok feldolgozását; egy hibás időzítő nem állítja le a programot.

## Fetch-hookok

A Phasis alapértelmezés szerint engedélyezett `fetch()` funkcióval érkezik, amelynek mögöttes átviteli rétege a PHP `ext-curl` eleme. Három hookkal cserélheti le a beágyazó az átviteli réteget, szabályozhatja a kéréseket, illetve engedélyezheti a sütik tárolását. Mindhárom elfogad PHP hívható objektumot vagy objektumon lévő metódust; az értékek alakjáról lásd a [Web APIs](https://phasis.inline0.dev/hu/docs/compatibility/web-apis) részt.

### setFetchTransport

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

Az alapértelmezett CurlTransport lecserélése. A hívható objektum egy kérésleírót és egy opcionális `AbortSignal` JsObject objektumot kap; válasza egy válaszleíró.

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

Gyakori beágyazási minta: teszteléskor álátviteli réteget, éles környezetben pedig valódi curl átviteli réteget telepítenek.

### setFetchPolicy

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

Kérés előtti házirend-hook. Megkapja a `Request` JsObject objektumot; hibát dobhat a tiltáshoz, módosított `Request` értéket adhat vissza az átíráshoz, vagy `null` értéket, illetve semmit a kérés engedélyezéséhez.

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

Opcionálisan engedélyezhető sütitároló. Bármely olyan objektumot elfogad, amely rendelkezik a `get(string $url): string` és a `set(string $url, string $header): void` elemmel — PHP-oldali vagy JS-oldali megoldást.

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

`setCookieJar()` nélkül a `fetch` figyelmen kívül hagyja a bejövő `Set-Cookie` és a kimenő `Cookie` fejléceket — alapértelmezés szerint **nincsenek sütik**.

## Kivételek

| Osztály | Kiterjeszti | Mikor |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | kifejezett JS `throw` esetén, amelyet semmi sem kapott el |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | a motor által kiváltott hiba esetén (például `null.x` vagy definiálatlan változó), amelyet semmi sem kapott el |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | a hurokkorlát vagy a hívásverem korlátjának túllépésekor |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | a fentiek alaposztálya |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | a forráskód nem értelmezhető |
| `\RuntimeException` | | a `execFile()` nem tudja beolvasni a fájlt; az eseményhurok elérte a 100,000 ciklusos korlátját |

A szkript minden hibájának elkapásához kapja el a `Phasis\Exceptions\RuntimeError` és a `Phasis\Exceptions\SyntaxError` hibákat, vagy egyszerűen a `\RuntimeException` osztályt.

A saját PHP hostfüggvényei által dobott kivételek másként működnek: jelenleg változatlanul jutnak át a JS `try` / `catch` rétegen, és az eredeti osztályukkal jutnak el a `eval()` hívójához ([issue #20](https://github.com/inline0/phasis/issues/20)). Lásd: [Host functions](https://phasis.inline0.dev/hu/docs/interop/host-functions#throwing-back-into-js).

## Életciklus

A `Engine` létrehozása gyors (hideg indításkor néhány ms), a standard könyvtár pedig lustán települ. A legtöbb beágyazási helyzetben folyamatonként elegendő egy hosszú életű motor. Több bérlős használat esetén minden bérlőhöz vagy kéréshez külön motort rendeljen, így a globális változók nem szivárognak át közöttük. Jelenleg új motor létrehozásával lehet megbízhatóan tiszta állapotból indulni (lásd: [reset](#reset)).

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