
# API

Le API d'exécution se compose d'une seule classe : `Phasis\Engine`. Tout le reste (`JsValue`, `JsFunction`, `JsObject`, …) est interne au moteur et n'est exposé qu'aux fonctions hôtes qui font le lien entre PHP et JS. L'autre interface publique est l'outil autonome [analyseur API](https://phasis.inline0.dev/fr/docs/parser) (les classes `Phasis\Parser\Parser` et `Phasis\Ast`), qui analyse le code source sans l'exécuter.

## Construction

```php
use Phasis\Engine;

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

Un `Engine` neuf expose toute la bibliothèque standard — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` — et les primitives du langage sont installées immédiatement pour que la spécification fonctionne (pas de classes sans `Function.prototype`, d'async/await sans `Promise` ni de littéraux d'expression régulière sans `RegExp`). Tout le reste — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray`, la famille des tampons de tableaux, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, la famille des références faibles, `Temporal`, `ShadowRealm`, le Web Platform Pack et le Fetch Pack — est enregistré comme accesseur de remplacement et instancié à la première lecture. La lecture du global est transparente : `Map`, `typeof Map`, `'Map' in globalThis`, `Object.keys(globalThis).includes('Map')` fonctionnent tous et, après la première lecture, l'accesseur de remplacement est remplacé par un descripteur de données classique.

Transmettez `eager: true` si vous avez spécifiquement besoin du comportement antérieur au chargement différé : chaque élément intégré est installé à la construction et toute sa surface de descripteurs est visible par introspection. Le harnais de conformité (`bin/test262`, `bin/wpt`) construit les moteurs en mode immédiat afin que test262/WPT voient les formes de descripteur attendues.

## eval

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

Analyse et exécute `$source` comme script classique dans la portée globale du moteur, puis exécute la boucle d'événements jusqu'à ce qu'elle soit vide. Renvoie la valeur de la dernière expression évaluée, convertie en PHP (ou `null` si le programme ne se termine pas par une expression). Voir [Conversion des valeurs](https://phasis.inline0.dev/fr/docs/interop/values) pour les règles.

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

Une Promise n'est pas déballée : `eval('Promise.resolve(5)')` renvoie un tableau vide. Comme la boucle d'événements est terminée lorsque `eval()` renvoie, stockez la valeur résolue dans un global et lisez-la avec un second `eval()` :

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

Une exception explicite JS `throw` qu'aucun code ne capture remonte sous la forme de `Phasis\Exceptions\JsThrowable`, dont le message est `"Name: message"` et dont la propriété `$jsValue` contient la valeur `JsValue` levée. Les erreurs générées par le moteur lui-même, par exemple la lecture d'une propriété de `null` ou une variable non définie, remontent plutôt sous la forme de `Phasis\Exceptions\TypeError`, `ReferenceError` ou `RangeError`. Toutes héritent de `Phasis\Exceptions\RuntimeError` ; les erreurs d'analyse héritent de `Phasis\Exceptions\SyntaxError`. Voir [Exceptions](#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
```

Évalue `$source` comme module ES : `import` / `export`, les `await` et `import.meta` au niveau supérieur sont disponibles, et les spécificateurs relatifs sont résolus par rapport à `$path` (un chemin de fichier absolu). Exécute la boucle d'événements comme `eval()` et renvoie toujours `null` ; lisez les résultats via les globals ou le tampon de console.

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

Le chargeur ne résout que les chemins relatifs et absolus ; voir [Limitations connues](https://phasis.inline0.dev/fr/docs/compatibility/limitations#module-loader).

## execFile

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

Lit un fichier sur le disque et transmet son contenu à `eval()`. Il s'exécute donc comme un script classique, quelle que soit son extension ; un fichier contenant `import` / `export` échoue avec une SyntaxError. Le chemin réel du fichier sert de base aux spécificateurs `import()` dynamiques. Utilisez `evalAsModule()` pour les fichiers de module.

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

Si le fichier est illisible, PHP émet un avertissement et `execFile()` lève une simple `\RuntimeException` (« Cannot read file: … »).

## setGlobal

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

Lie une variable globale visible lors de toutes les évaluations JS suivantes. Les valeurs PHP sont converties en JS :

| Type PHP | Type JS |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (même si le nom désigne une fonction, comme `'strlen'`) |
| tableau (liste) | `Array` (copie) |
| tableau (associatif) | `Object` (copie) |
| `Closure`, objet invocable, `[$object, 'method']` | `function` |
| tout autre objet, générateur ou ressource | `undefined` |

Les tableaux sont copiés : les modifications effectuées par JS n'apparaissent pas dans le tableau PHP. Les objets PHP ordinaires ne peuvent pas encore être transmis ([issue #18](https://github.com/inline0/phasis/issues/18)) ; voir [Objets partagés](https://phasis.inline0.dev/fr/docs/interop/shared-objects) pour des solutions de contournement.

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

Dans JS :

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

## call

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

Appelle une fonction globale JS avec les arguments PHP (convertis comme pour `setGlobal`). La valeur renvoyée est reconvertie en PHP. Appeler un nom non défini lève `Phasis\Exceptions\ReferenceError` ; appeler une variable globale qui n'est pas une fonction lève `Phasis\Exceptions\TypeError`.

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

## Sortie de la console

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

`console.log`, `console.info`, `console.warn`, `console.error` et `console.debug` n'affichent rien. Chaque appel ajoute une ligne formatée à un tampon du moteur. `getConsoleLines()` renvoie les lignes du tampon sous forme de liste de chaînes, `getConsoleOutput()` les renvoie réunies avec `\n` et `clearConsole()` vide le tampon. Celui-ci n'est pas effacé entre les appels à `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
```

Définit une limite de ressource réglable à l'exécution. Il n'y en a qu'une :

| Limite | Valeur par défaut | Description |
|---|---|---|
| `maxLoopIterations` | `100000` | nombre maximal d'itérations par exécution de boucle `for` / `while` / `do…while` dans l'interpréteur à parcours d'arbre |

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

Tout autre nom est ignoré silencieusement ; aucune limite ne s'applique à la longueur des chaînes, à la sortie de la console ou au temps réel écoulé.

La limite de boucle n'est pas encore appliquée partout. Les boucles exécutées par la VM bytecode — qui couvre les corps de fonctions et les programmes de niveau supérieur acceptés par le compilateur bytecode — ne sont jamais comptabilisées. Ainsi, `while (true) {}` continue jusqu'à ce que PHP l'arrête ([issue #19](https://github.com/inline0/phasis/issues/19)). Ne comptez pas sur cette limite pour arrêter du code non fiable ; utilisez PHP et son `max_execution_time` ou un processus distinct.

Lorsque la limite est atteinte, l'erreur peut être capturée par un JS `try` / `catch` (sous forme d'un `Error` dont le message est « Maximum loop iterations exceeded »). Si rien ne la capture, elle atteint PHP sous forme de `Phasis\Exceptions\InternalError`, qui hérite de `Phasis\Exceptions\RuntimeError` et non de `JsThrowable`.

Deux autres plafonds sont intégrés au moteur et ne peuvent pas être réglés à l'exécution : `Phasis\Runtime\CallStack::maxDepth` (4,096 cadres JS) et la limite de profondeur des fermetures transpilées JsToPhp (8,192 cadres PHP). Le dépassement de l'un ou l'autre lève « Maximum call stack size exceeded », qui remonte également jusqu'à PHP sous forme de `InternalError`. Ces plafonds sont bien supérieurs aux besoins de tout programme JS légitime ; les relever nécessite de modifier le code, pas la configuration.

## reset

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

Efface tous les globals définis par l'utilisateur et le tampon de console, puis recrée la bibliothèque standard et la boucle d'événements.

Sur un moteur construit en mode différé par défaut, `reset()` perd actuellement tous les éléments intégrés installés à la demande : ensuite, `Map`, `JSON`, `Math`, `console`, `fetch` et les autres ne sont plus définis ([issue #22](https://github.com/inline0/phasis/issues/22)). En attendant le correctif, construisez plutôt un nouvel `Engine` ou utilisez `eager: true` à la construction : `reset()` fonctionne alors.

## Boucle d'événements

Le moteur inclut une boucle d'événements JavaScript avec `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` et `queueMicrotask` sur l'objet global. Les microtâches (gestionnaires de Promise, `queueMicrotask`) sont exécutées entre chaque macrotâche ; les minuteurs se déclenchent par ordre d'échéance dans une file de tâches propre à chaque contexte.

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

`eval()` et `evalAsModule()` appellent automatiquement `runEventLoop()` à la fin du code synchrone. Ainsi, un script qui planifie `setTimeout(cb, 5000)` au niveau supérieur l’exécute `cb` avant que `eval()` ne renvoie la main : la boucle utilise le **temps virtuel** et avance l’horloge jusqu’à la prochaine échéance, sans attendre PHP. Conforme à la sémantique de `node script.js`.

Pour une intégration en temps réel (serveurs de longue durée, intégration UI, planification selon une horloge réelle), `tickEventLoop()` exécute une itération (vide les microtâches, déclenche les minuteurs arrivés à échéance), puis renvoie. `pendingTaskCount()` indique les minuteurs encore en attente ; le programme hôte peut gérer sa propre boucle :

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

Une `setInterval` qui s'emballe est limitée : `runEventLoop()` plafonne à 100,000 cycles et lève une simple `\RuntimeException` — le code JS fautif génère ainsi une erreur PHP claire au lieu de bloquer. L'intervalle reste planifié : tous les appels ultérieurs à `eval()` sur le même moteur lèvent donc la même erreur. Écartez ensuite le moteur. Les erreurs non capturées dans un rappel de minuteur sont émises via `error_log()` et la boucle continue ; un minuteur défectueux ne bloque pas le programme.

## Points d'extension Fetch

Phasis inclut `fetch()`, activé par défaut, avec le `ext-curl` de PHP comme transport sous-jacent. Trois points d'extension permettent de remplacer le transport, de contrôler les requêtes ou d'activer le stockage des cookies. Ils acceptent tous un appelable PHP ou une méthode d'objet ; voir [API Web](https://phasis.inline0.dev/fr/docs/compatibility/web-apis) pour le contrat de forme des valeurs.

### setFetchTransport

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

Remplace CurlTransport par défaut. L'appelable reçoit un descripteur de requête et un `AbortSignal` JsObject facultatif ; il renvoie un descripteur de réponse.

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

Modèle d'intégration courant : installer un transport simulé dans les tests et le transport Curl réel en production.

### setFetchPolicy

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

Point d'extension de la politique de pré-vérification. Reçoit le JsObject `Request` ; peut lever une exception pour refuser, renvoyer un `Request` modifié pour réécrire la requête, ou renvoyer `null`/aucune valeur pour l'autoriser.

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

Stockage des cookies activé sur demande. Accepte tout objet exposant `get(string $url): string` et `set(string $url, string $header): void`, côté PHP ou côté 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());
```

Sans `setCookieJar()`, `fetch` ignore les en-têtes `Set-Cookie` entrants et `Cookie` sortants : par défaut, **aucun cookie** n'est utilisé.

## Exceptions

| Classe | Hérite de | Cas |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | exception explicite JS `throw` non capturée |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | erreur générée par le moteur (par exemple `null.x` ou une variable non définie) non capturée |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | dépassement de la limite de boucle ou du plafond de pile d'appels |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | classe de base des classes ci-dessus |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | le code source ne peut pas être analysé |
| `\RuntimeException` | | `execFile()` ne peut pas lire le fichier ; la boucle d'événements a atteint sa limite de 100,000 cycles |

Pour capturer toutes les erreurs d'un script, capturez `Phasis\Exceptions\RuntimeError` et `Phasis\Exceptions\SyntaxError`, ou utilisez simplement `\RuntimeException`.

Les exceptions levées par vos propres fonctions hôtes PHP sont différentes : elles traversent actuellement JS `try` / `catch` sans modification et atteignent l'appelant de `eval()` avec leur classe d'origine ([issue #20](https://github.com/inline0/phasis/issues/20)). Voir [Fonctions hôtes](https://phasis.inline0.dev/fr/docs/interop/host-functions#throwing-back-into-js).

## Cycle de vie

`Engine` se construit rapidement (quelques ms à froid) et installe la bibliothèque standard à la demande. Pour la plupart des intégrations, un seul moteur de longue durée par processus suffit. Pour une utilisation multi-tenant, attribuez un moteur à chaque tenant ou requête afin d'éviter toute fuite de globals entre eux. Aujourd'hui, la solution fiable pour repartir d'un état vierge consiste à construire un nouveau moteur (voir [reset](#reset)).

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