
# API

Execuția API folosește o singură clasă: `Phasis\Engine`. Tot restul (`JsValue`, `JsFunction`, `JsObject`, …) este intern motorului și este expus doar funcțiilor gazdă care fac legătura între PHP↔JS. Singura altă interfață publică este [parserul de sine stătător API](https://phasis.inline0.dev/ro/docs/parser) (`Phasis\Parser\Parser` și clasele `Phasis\Ast`), care analizează sursa fără să o execute.

## Construire

```php
use Phasis\Engine;

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

Un `Engine` nou expune biblioteca standard completă — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error`, iar intrinsecurile limbajului de bază sunt instalate imediat, astfel încât specificația să funcționeze (nu puteți avea clase fără `Function.prototype`, async/await fără `Promise` sau literali regex fără `RegExp`). Toate celelalte — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` și familia array-buffer, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, familia weak-ref, `Temporal`, `ShadowRealm`, Web Platform Pack și Fetch Pack — sunt înregistrate ca accesor placeholder și materializate la prima citire. Citirea din global este transparentă: `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')` funcționează, iar după prima citire placeholder-ul este înlocuit cu un descriptor de date obișnuit.

Transmiteți `eager: true` dacă aveți nevoie în mod special de comportamentul anterior încărcării leneșe — toate funcționalitățile încorporate sunt instalate la construire, iar întreaga suprafață a descriptorilor este vizibilă la introspecție. Harness-ul de conformitate (`bin/test262`, `bin/wpt`) construiește motoarele în modul eager, astfel încât test262/WPT să vadă formele descriptorilor așteptate.

## eval

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

Analizează și execută `$source` ca script clasic în domeniul global al motorului, apoi rulează bucla de evenimente până se golește. Returnează valoarea ultimei expresii evaluate, convertită în PHP (sau `null` dacă programul nu are o expresie finală). Consultați [Conversia valorilor](https://phasis.inline0.dev/ro/docs/interop/values) pentru reguli.

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

Un Promise nu este despachetat: `eval('Promise.resolve(5)')` returnează un tablou gol. Deoarece bucla de evenimente s-a golit până când se întoarce `eval()`, stocați valoarea stabilită într-un global și citiți-o cu un al doilea `eval()`:

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

Un JS `throw` explicit pe care nu îl prinde nimic este expus ca `Phasis\Exceptions\JsThrowable`, al cărui mesaj este `"Name: message"` și a cărui proprietate `$jsValue` conține valoarea `JsValue` aruncată. Erorile generate de motor, precum citirea unei proprietăți a lui `null` sau folosirea unei variabile nedefinite, sunt expuse în schimb ca `Phasis\Exceptions\TypeError`, `ReferenceError` sau `RangeError`. Toate acestea extind `Phasis\Exceptions\RuntimeError`; erorile de analiză extind `Phasis\Exceptions\SyntaxError`. Consultați [Excepții](#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
```

Evaluează `$source` ca modul ES: sunt disponibile `import` / `export` și `await` și `import.meta` la nivel superior, iar specificatorii relativi sunt rezolvați în raport cu `$path` (o cale absolută de fișier). Rulează bucla de evenimente ca `eval()` și returnează întotdeauna `null`; citiți rezultatele prin variabile globale sau din memoria tampon a consolei.

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

Încărcătorul rezolvă doar căi relative și absolute; consultați [Limitări cunoscute](https://phasis.inline0.dev/ro/docs/compatibility/limitations#module-loader).

## execFile

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

Citește un fișier de pe disc și îi transmite conținutul către `eval()`, astfel încât să ruleze ca script clasic indiferent de extensie; un fișier cu `import` / `export` eșuează cu o eroare SyntaxError. Calea reală a fișierului devine baza pentru specificatorii `import()` dinamici. Folosiți `evalAsModule()` pentru fișiere modul.

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

Dacă fișierul nu poate fi citit, PHP emite un avertisment, iar `execFile()` aruncă o excepție `\RuntimeException` simplă („Cannot read file: …”).

## setGlobal

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

Asociază o variabilă globală vizibilă în toate evaluările JS ulterioare. Valorile PHP sunt convertite în JS:

| Tipul PHP | Tipul JS |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (chiar și când denumește o funcție, precum `'strlen'`) |
| tablou (listă) | `Array` (o copie) |
| tablou (asociativ) | `Object` (o copie) |
| `Closure`, obiect apelabil, `[$object, 'method']` | `function` |
| orice alt obiect, generator sau resursă | `undefined` |

Tabelele sunt copiate: modificările pe care le face JS nu sunt vizibile în tabloul PHP. Obiectele simple PHP nu pot fi transmise încă ([problema #18](https://github.com/inline0/phasis/issues/18)); consultați [Obiecte partajate](https://phasis.inline0.dev/ro/docs/interop/shared-objects) pentru soluții alternative.

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

În interiorul JS:

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

## call

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

Apelează o funcție globală JS cu argumente PHP (convertite ca pentru `setGlobal`). Valoarea returnată este convertită înapoi în PHP. Apelarea unui nume nedefinit aruncă `Phasis\Exceptions\ReferenceError`; apelarea unei variabile globale care nu este funcție aruncă `Phasis\Exceptions\TypeError`.

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

## Ieșirea consolei

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

`console.log`, `console.info`, `console.warn`, `console.error` și `console.debug` nu afișează nimic. Fiecare apel adaugă o linie formatată într-o memorie tampon a motorului. `getConsoleLines()` returnează liniile din memorie ca listă de șiruri, `getConsoleOutput()` le returnează unite cu `\n`, iar `clearConsole()` golește memoria tampon. Aceasta nu este golită între apelurile `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
```

Setează o limită de resurse configurabilă în timpul execuției. Există una singură:

| Limită | Implicită | Descriere |
|---|---|---|
| `maxLoopIterations` | `100000` | numărul maxim de iterații pentru fiecare rulare a buclei `for` / `while` / `do…while` de către interpretorul cu parcurgere arborescentă |

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

Orice alt nume este ignorat fără avertisment; nu există limite pentru lungimea șirurilor, ieșirea consolei sau timpul real de execuție.

Limita buclei nu este încă aplicată peste tot. Buclelele rulate pe mașina virtuală de cod octet, care acoperă corpurile funcțiilor și programele de nivel superior acceptate de compilatorul de cod octet, nu sunt numărate niciodată, așa că `while (true) {}` rulează până când PHP îl oprește ([problema #19](https://github.com/inline0/phasis/issues/19)). Nu vă bazați pe aceasta pentru a opri codul care nu este de încredere; folosiți PHP `max_execution_time` sau un proces separat.

Când limita este atinsă, eroarea poate fi prinsă de un JS `try` / `catch` (ca `Error` cu mesajul „Maximum loop iterations exceeded”). Dacă nu o prinde nimic, ajunge la PHP ca `Phasis\Exceptions\InternalError`, care extinde `Phasis\Exceptions\RuntimeError`, nu `JsThrowable`.

Alte două limite maxime sunt încorporate în motor și nu pot fi configurate în timpul execuției: `Phasis\Runtime\CallStack::maxDepth` (4,096 cadre JS) și limita de adâncime pentru closure-urile transpuse JsToPhp (8,192 cadre PHP). Depășirea oricăreia aruncă „Maximum call stack size exceeded”, care ajunge tot ca `InternalError` la PHP. Aceste limite sunt mult peste necesarul oricărui program JS legitim — modificarea lor necesită schimbarea codului, nu a unei opțiuni de configurare.

## reset

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

Șterge toate variabilele globale definite de utilizator și memoria tampon a consolei, apoi reconstruiește biblioteca standard și bucla de evenimente.

În prezent, pe un motor construit în modul leneș implicit, `reset()` pierde toate funcționalitățile încorporate instalate leneș: ulterior `Map`, `JSON`, `Math`, `console`, `fetch` și restul sunt nedefinite ([problema #22](https://github.com/inline0/phasis/issues/22)). Până la remedierea problemei, construiți în schimb un `Engine` nou sau construiți cu `eager: true`, unde `reset()` funcționează.

## Bucla de evenimente

Motorul include o buclă de evenimente JavaScript, cu `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` și `queueMicrotask` pe obiectul global. Microtask-urile (handler-ele Promise, `queueMicrotask`) sunt procesate între fiecare macrotask; temporizatoarele se declanșează în ordinea termenelor printr-o coadă de task-uri proprie fiecărui realm.

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

`eval()` și `evalAsModule()` apelează automat `runEventLoop()` după finalizarea codului sincron. Astfel, un script care programează `setTimeout(cb, 5000)` la nivel superior rulează `cb` înainte să se întoarcă `eval()` — bucla folosește **timp virtual** și își avansează ceasul la următorul termen în așteptare, fără să aștepte PHP. Respectă semantica `node script.js`.

Pentru integrarea în timp real (servere cu durată lungă de funcționare, integrare UI, programare după un ceas real), `tickEventLoop()` rulează o iterație (procesează microtask-urile și declanșează temporizatoarele ajunse la termen), apoi returnează. `pendingTaskCount()` raportează temporizatoarele restante; aplicația gazdă își poate rula propria buclă:

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

Un `setInterval` care nu se oprește este limitat: `runEventLoop()` se oprește la 100,000 de cicluri și aruncă o excepție simplă `\RuntimeException` — eroarea din codul JS este semnalată clar ca eroare PHP, în loc să blocheze execuția. Intervalul rămâne programat, așa că fiecare apel ulterior la `eval()` pe același motor aruncă aceeași eroare; renunțați apoi la motor. Erorile neprinse din callback-urile temporizatoarelor sunt emise prin `error_log()`, iar bucla continuă să proceseze task-urile; un temporizator defect nu oprește programul.

## Hook-uri Fetch

Phasis include `fetch()` activat implicit, cu `ext-curl` din PHP ca transport de bază. Trei hook-uri permit aplicației gazdă să schimbe transportul, să controleze solicitările sau să activeze stocarea cookie-urilor. Toate acceptă fie un callable PHP, fie o metodă a unui obiect; consultați [API-uri web](https://phasis.inline0.dev/ro/docs/compatibility/web-apis) pentru contractul privind forma valorilor.

### setFetchTransport

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

Înlocuiește CurlTransport implicit. Callable-ul primește un descriptor de solicitare și un JsObject `AbortSignal` opțional; returnează un descriptor de răspuns.

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

Un tipar obișnuit de integrare: instalați un transport simulat în teste și transportul curl real în producție.

### setFetchPolicy

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

Hook de politică înaintea solicitării. Primește JsObject-ul `Request`; poate arunca o excepție pentru a o refuza, poate returna un `Request` modificat pentru a o rescrie sau poate returna `null`/nicio valoare pentru a o permite.

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

Stocare opțională pentru cookie-uri. Acceptă orice obiect care expune `get(string $url): string` și `set(string $url, string $header): void` — pe partea PHP sau pe partea JS.

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

Fără `setCookieJar()`, `fetch` ignoră atât antetele `Set-Cookie` de intrare, cât și antetele `Cookie` de ieșire — implicit, **nu sunt folosite cookie-uri**.

## Excepții

| Clasă | Extinde | Când |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | un JS `throw` explicit pe care nu l-a prins nimic |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | o eroare generată de motor (de exemplu, `null.x` sau o variabilă nedefinită) pe care nu a prins-o nimic |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | limita buclei sau limita stivei de apeluri a fost depășită |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | clasa de bază a celor de mai sus |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | sursa nu poate fi analizată |
| `\RuntimeException` | | `execFile()` nu poate citi fișierul; bucla de evenimente a atins limita de 100,000 cicluri |

Pentru a prinde toate erorile unui script, prindeți `Phasis\Exceptions\RuntimeError` și `Phasis\Exceptions\SyntaxError` sau folosiți pur și simplu `\RuntimeException`.

Excepțiile aruncate de propriile funcții gazdă PHP sunt diferite: în prezent trec prin JS `try` / `catch` și ajung neschimbate la apelantul lui `eval()`, păstrându-și clasa inițială ([problema #20](https://github.com/inline0/phasis/issues/20)). Consultați [Funcții gazdă](https://phasis.inline0.dev/ro/docs/interop/host-functions#throwing-back-into-js).

## Ciclul de viață

`Engine` se construiește rapid (~câteva ms la rece), iar biblioteca standard se instalează leneș. Pentru majoritatea scenariilor de integrare, este potrivit un singur motor cu durată lungă de funcționare per proces. Pentru utilizarea cu mai mulți clienți, alocați fiecărui client sau fiecărei solicitări propriul motor, astfel încât variabilele globale să nu se transfere între ele. În prezent, construirea unui motor nou este metoda sigură de a porni de la zero (consultați [reset](#reset)).

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