Aller au contenu

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 (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 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.

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.

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) ; voir Objets partagés 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). 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). 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 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). Voir Fonctions hôtes.

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

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