
# API

L’API di esecuzione è una classe: `Phasis\Engine`. Tutto il resto (`JsValue`, `JsFunction`, `JsObject`, …) è interno al motore ed è esposto solo alle funzioni host che fanno da ponte tra PHP↔JS. L’unica altra interfaccia pubblica è il [parser autonomo](https://phasis.inline0.dev/it/docs/parser) API (le classi `Phasis\Parser\Parser` e `Phasis\Ast`), che analizza il codice sorgente senza eseguirlo.

## Creazione

```php
use Phasis\Engine;

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

Una nuova istanza di `Engine` espone la libreria standard completa: `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` e le primitive del linguaggio principale vengono installate subito, per garantire il funzionamento della specifica (senza `Function.prototype` non si possono avere classi, senza `Promise` async/await e senza `RegExp` i letterali regex). Tutto il resto — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` e la famiglia degli array buffer, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, la famiglia weak-ref, `Temporal`, `ShadowRealm`, il Web Platform Pack e il Fetch Pack — viene registrato come accessor segnaposto e materializzato alla prima lettura. L’accesso alle variabili globali è trasparente: `Map`, `typeof Map`, `'Map' in globalThis` e `Object.keys(globalThis).includes('Map')` funzionano tutti; dopo la prima lettura, il segnaposto viene sostituito da un normale descrittore dati.

Passate `eager: true` se vi serve espressamente il comportamento precedente al caricamento lazy: ogni funzione integrata viene installata alla creazione, con l’intera superficie del descrittore disponibile per l’introspezione. Il test harness di conformità (`bin/test262`, `bin/wpt`) crea gli engine in modalità eager, affinché test262/WPT rilevi le forme dei descrittori previste.

## eval

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

Analizza ed esegue `$source` come script classico nell’ambito globale dell’engine, poi esegue il ciclo degli eventi finché non è vuoto. Restituisce il valore dell’ultima espressione valutata, convertito in PHP (oppure `null` se il programma non termina con un’espressione). Per le regole, vedi [Conversione dei valori](https://phasis.inline0.dev/it/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)
```

Una Promise non viene risolta automaticamente: `eval('Promise.resolve(5)')` restituisce un array vuoto. Poiché il ciclo degli eventi si è svuotato quando `eval()` restituisce, memorizzate il valore risolto in una variabile globale e leggetelo con una seconda chiamata a `eval()`:

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

Un JS `throw` esplicito non intercettato viene restituito come `Phasis\Exceptions\JsThrowable`, il cui messaggio è `"Name: message"` e la cui proprietà `$jsValue` contiene il valore `JsValue` lanciato. Gli errori generati direttamente dall’engine, per esempio accedendo a una proprietà di `null` o a una variabile non definita, vengono restituiti invece come `Phasis\Exceptions\TypeError`, `ReferenceError` o `RangeError`. Tutti questi estendono `Phasis\Exceptions\RuntimeError`; gli errori di analisi estendono `Phasis\Exceptions\SyntaxError`. Vedi [Eccezioni](#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
```

Valuta `$source` come modulo ES: sono disponibili `import` / `export` e le istruzioni `await` e `import.meta` a livello superiore; gli identificatori relativi vengono risolti rispetto a `$path` (un percorso assoluto al file). Esegue il ciclo degli eventi come `eval()` e restituisce sempre `null`; leggete i risultati tramite le variabili globali o il buffer della console.

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

Il caricatore risolve solo percorsi relativi e assoluti; vedi [Limitazioni note](https://phasis.inline0.dev/it/docs/compatibility/limitations#module-loader).

## execFile

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

Legge un file dal disco e ne passa il contenuto a `eval()`, che lo esegue come script classico a prescindere dall’estensione; un file con `import` / `export` genera un SyntaxError. Il percorso reale del file diventa la base per gli identificatori `import()` dinamici. Per i file di modulo, usate `evalAsModule()`.

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

Se non è possibile leggere il file, PHP emette un avviso e `execFile()` lancia un semplice `\RuntimeException` ("Impossibile leggere il file: …").

## setGlobal

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

Associa una variabile globale, visibile in tutte le successive valutazioni di JS. I valori PHP vengono convertiti in JS:

| Tipo PHP | Tipo JS |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (anche quando indica una funzione, come `'strlen'`) |
| array (lista) | `Array` (una copia) |
| array (associativo) | `Object` (una copia) |
| `Closure`, oggetto invocabile, `[$object, 'method']` | `function` |
| qualsiasi altro oggetto, generatore o risorsa | `undefined` |

Gli array vengono copiati: le modifiche apportate da JS non sono visibili nell’array PHP. Per ora non è possibile passare oggetti semplici PHP ([problema n. 18](https://github.com/inline0/phasis/issues/18)); per le alternative, vedi [Oggetti condivisi](https://phasis.inline0.dev/it/docs/interop/shared-objects).

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

All’interno di JS:

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

## call

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

Chiama una funzione globale JS con argomenti PHP (convertiti come per `setGlobal`). Il valore restituito viene riconvertito in PHP. Chiamare un nome non definito genera `Phasis\Exceptions\ReferenceError`; chiamare una variabile globale che non è una funzione genera `Phasis\Exceptions\TypeError`.

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

## Output della console

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

`console.log`, `console.info`, `console.warn`, `console.error` e `console.debug` non stampano a schermo. Ogni chiamata aggiunge una riga formattata a un buffer dell’engine. `getConsoleLines()` restituisce le righe memorizzate come elenco di stringhe, `getConsoleOutput()` le restituisce unite con `\n` e `clearConsole()` svuota il buffer. Il buffer non viene cancellato tra una chiamata `eval()` e l’altra.

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

Imposta un limite alle risorse modificabile a runtime. È disponibile esattamente un limite:

| Limite | Predefinito | Descrizione |
|---|---|---|
| `maxLoopIterations` | `100000` | numero massimo di iterazioni per ogni esecuzione di un ciclo `for` / `while` / `do…while` gestito dall'interprete tree-walking |

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

Qualsiasi altro nome viene ignorato senza avvisi; non ci sono limiti alla lunghezza delle stringhe, all'output della console o al tempo di esecuzione.

Il limite dei cicli non è ancora applicato ovunque. I cicli eseguiti dalla VM a bytecode, che comprende i corpi delle funzioni e i programmi di primo livello accettati dal compilatore bytecode, non vengono mai conteggiati, quindi `while (true) {}` continua a essere eseguito finché PHP non lo interrompe ([issue #19](https://github.com/inline0/phasis/issues/19)). Non affidatevi a questo limite per fermare codice non attendibile: usate PHP's `max_execution_time` oppure un processo separato.

Quando si raggiunge il limite, l'errore può essere intercettato da un JS `try` / `catch` (come `Error` con il messaggio "Maximum loop iterations exceeded"). Se non viene intercettato, raggiunge PHP come `Phasis\Exceptions\InternalError`, che estende `Phasis\Exceptions\RuntimeError`, non `JsThrowable`.

Altri due limiti sono integrati nel motore e non sono modificabili a runtime: `Phasis\Runtime\CallStack::maxDepth` (4,096 frame JS) e il limite di profondità per le closure transpilate da JsToPhp (8,192 frame PHP). Se superati, entrambi generano "Maximum call stack size exceeded", che raggiunge anche PHP come `InternalError`. Sono molto al di sopra di qualsiasi programma JS legittimo: aumentarli richiede una modifica al codice, non un'opzione di configurazione.

## reset

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

Cancella tutte le variabili globali definite dall'utente e il buffer della console, quindi ricrea la libreria standard e il ciclo di eventi.

Su un motore creato in modalità lazy predefinita, al momento `reset()` rimuove tutte le funzioni integrate installate pigramente: in seguito, `Map`, `JSON`, `Math`, `console`, `fetch` e le altre risultano undefined ([issue #22](https://github.com/inline0/phasis/issues/22)). Finché il problema non viene risolto, create invece un nuovo `Engine` oppure istanziatelo con `eager: true`, modalità in cui `reset()` funziona.

## Ciclo di eventi

Il motore include un ciclo di eventi JavaScript con `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` e `queueMicrotask` sull'oggetto globale. Le microtask (gestori Promise, `queueMicrotask`) vengono eseguite tra ogni macrotask; i timer scattano in ordine di scadenza tramite una coda di attività specifica per realm.

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

`eval()` e `evalAsModule()` chiamano automaticamente `runEventLoop()` al termine del codice sincrono. Quindi uno script che pianifica `setTimeout(cb, 5000)` al livello superiore viene eseguito `cb` prima che `eval()` restituisca il controllo: il ciclo usa il **tempo virtuale** e porta l'orologio alla prossima scadenza in sospeso, senza attendere PHP. Conforme alla semantica di `node script.js`.

Per l'integrazione in tempo reale (server di lunga durata, integrazione UI, pianificazione basata su un orologio reale), `tickEventLoop()` esegue una iterazione (svuota le microtask, attiva gli eventuali timer scaduti) e restituisce il controllo. `pendingTaskCount()` segnala i timer ancora in sospeso; chi integra il motore può gestire il proprio ciclo:

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

Un `setInterval` senza fine è limitato: `runEventLoop()` si arresta dopo 100,000 cicli e genera un semplice `\RuntimeException`; l'errore nel codice JS viene segnalato chiaramente come errore PHP anziché bloccare l'esecuzione. L'intervallo resta pianificato, quindi ogni successivo `eval()` sullo stesso motore genera lo stesso errore; in seguito, scartate il motore. Gli errori non intercettati nei callback dei timer vengono emessi tramite `error_log()` e il ciclo continua; un timer difettoso non interrompe il programma.

## Hook Fetch

Phasis include `fetch()` attivo per impostazione predefinita, con `ext-curl` di PHP come trasporto sottostante. Tre hook consentono di sostituire il trasporto, filtrare le richieste o attivare la memorizzazione dei cookie. Tutti e tre accettano una funzione PHP oppure un metodo di un oggetto; per il contratto sui formati dei valori, consultate [Web APIs](https://phasis.inline0.dev/it/docs/compatibility/web-apis).

### setFetchTransport

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

Sostituisce il CurlTransport predefinito. La funzione riceve un descrittore della richiesta e un `AbortSignal` JsObject facoltativo; restituisce un descrittore della risposta.

```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 modello comune di integrazione: installare un trasporto simulato nei test e il trasporto curl reale in produzione.

### setFetchPolicy

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

Hook per i criteri di verifica preliminare. Riceve il `Request` JsObject; può generare un errore per negare la richiesta, restituire un `Request` modificato per riscriverla oppure restituire `null`/nessun valore per consentirla.

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

Archivio cookie facoltativo. Accetta qualsiasi oggetto che esponga `get(string $url): string` e `set(string $url, string $header): void`, lato PHP o 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());
```

Senza `setCookieJar()`, `fetch` ignora sia gli header `Set-Cookie` in ingresso sia quelli `Cookie` in uscita: per impostazione predefinita i cookie sono **disattivati**.

## Eccezioni

| Classe | Estende | Quando |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | viene generato un JS `throw` esplicito che non viene intercettato |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | errore generato dal motore (ad esempio `null.x` o una variabile non definita) che non viene intercettato |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | viene superato il limite dei cicli o il limite dello stack di chiamate |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | classe base delle precedenti |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | il sorgente non può essere analizzato |
| `\RuntimeException` | | `execFile()` non riesce a leggere il file; il ciclo di eventi ha raggiunto il limite di 100,000 cicli |

Per intercettare tutti gli errori di uno script, intercettate `Phasis\Exceptions\RuntimeError` e `Phasis\Exceptions\SyntaxError` oppure semplicemente `\RuntimeException`.

Le eccezioni generate dalle vostre funzioni host PHP sono diverse: al momento passano attraverso JS `try` / `catch` e raggiungono invariato il chiamante di `eval()`, mantenendo la classe originale ([issue #20](https://github.com/inline0/phasis/issues/20)). Consultate [Funzioni host](https://phasis.inline0.dev/it/docs/interop/host-functions#throwing-back-into-js).

## Ciclo di vita

`Engine` si crea rapidamente (pochi ms a freddo) e installa pigramente la libreria standard. Nella maggior parte degli scenari di integrazione è sufficiente un unico motore di lunga durata per processo. Per l'uso multi-tenant, assegnate a ogni tenant o richiesta un motore proprio, così le variabili globali non si condividono. Creare un nuovo motore è oggi il modo più affidabile per ripartire da uno stato pulito (vedi [reset](#reset)).

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