Saltar al contenido

API

La API de ejecución consta de una sola clase: Phasis\Engine. Todo lo demás (JsValue, JsFunction, JsObject, …) es interno del motor y solo se expone a las funciones anfitrionas que conectan PHP↔JS. La otra interfaz pública es el analizador independiente API (las clases Phasis\Parser\Parser y Phasis\Ast), que analiza código fuente sin ejecutarlo.

Construcción

PHP
use Phasis\Engine;

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

Una instancia nueva de Engine expone la biblioteca estándar completa: Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error y las funciones intrínsecas del lenguaje base se instalan de inmediato para que la especificación funcione (no puede haber clases sin Function.prototype, async/await sin Promise ni literales de expresión regular sin RegExp). Todo lo demás —Math, JSON, Map, Set, Date, TypedArray y la familia de búferes de arrays, Atomics, Proxy, Reflect, console, Intl, la familia de referencias débiles, Temporal, ShadowRealm, Web Platform Pack y Fetch Pack— se registra como un accesor de marcador de posición y se materializa en la primera lectura. La lectura del objeto global es transparente: Map, typeof Map, 'Map' in globalThis y Object.keys(globalThis).includes('Map') funcionan, y tras la primera lectura el marcador de posición se sustituye por un descriptor de datos normal.

Pase eager: true si necesita específicamente el comportamiento anterior a la carga diferida: todas las funciones integradas se instalan al construir la instancia y toda la superficie de descriptores queda visible para la introspección. El arnés de conformidad (bin/test262, bin/wpt) construye los motores en modo inmediato para que test262/WPT vean las formas de descriptor esperadas.

eval

PHP
public function eval(string $source): mixed

Analiza y ejecuta $source como un script clásico en el ámbito global del motor y, después, ejecuta el bucle de eventos hasta vaciarlo. Devuelve el valor de la última expresión evaluada, convertido a PHP (o null si el programa no termina con una expresión). Consulte las reglas en Conversión de valores.

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 no se desenvuelve: eval('Promise.resolve(5)') devuelve un array vacío. Como el bucle de eventos ya se ha agotado cuando termina eval(), guarde el valor resuelto en una variable global y léalo con un segundo eval():

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

Un JS throw explícito que no captura nada aparece como Phasis\Exceptions\JsThrowable, cuyo mensaje es "Name: message" y cuya propiedad $jsValue contiene el JsValue lanzado. Los errores que genera el propio motor, como leer una propiedad de null o usar una variable no definida, aparecen como Phasis\Exceptions\TypeError, ReferenceError o RangeError. Todos estos extienden Phasis\Exceptions\RuntimeError; los errores de análisis extienden Phasis\Exceptions\SyntaxError. Consulte Excepciones.

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

Evalúa $source como un módulo ES: están disponibles import / export y await y import.meta de nivel superior; los especificadores relativos se resuelven respecto a $path (una ruta de archivo absoluta). Ejecuta el bucle de eventos como eval() y siempre devuelve null; lea los resultados mediante variables globales o el búfer de la consola.

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

El cargador solo resuelve rutas relativas y absolutas; consulte Limitaciones conocidas.

execFile

PHP
public function execFile(string $path): mixed

Lee un archivo del disco y pasa su contenido a eval(), por lo que se ejecuta como script clásico, sea cual sea su extensión; un archivo con import / export produce un SyntaxError. La ruta real del archivo se convierte en la base para los especificadores import() dinámicos. Use evalAsModule() para archivos de módulos.

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

Si no se puede leer el archivo, PHP emite una advertencia y execFile() lanza un \RuntimeException simple ("Cannot read file: …").

setGlobal

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

Vincula una variable global visible en todas las evaluaciones posteriores de JS. Los valores de PHP se convierten a JS:

Tipo de PHP Tipo de JS
null null
bool boolean
int / float number
string string (incluso cuando nombra una función, como 'strlen')
array (lista) Array (una copia)
array (asociativo) Object (una copia)
Closure, objeto invocable, [$object, 'method'] function
cualquier otro objeto, generador o recurso undefined

Los arrays se copian: los cambios que JS les haga no se reflejan en el array PHP. Aún no se pueden pasar objetos PHP simples (incidencia #18); consulte Objetos compartidos para ver soluciones alternativas.

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

Dentro de JS:

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

call

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

Llama a una función global JS con argumentos PHP (convertidos como en setGlobal). El valor devuelto se convierte de nuevo a PHP. Llamar a un nombre no definido lanza Phasis\Exceptions\ReferenceError; llamar a una variable global que no sea una función lanza Phasis\Exceptions\TypeError.

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

Salida de la consola

PHP
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void

console.log, console.info, console.warn, console.error y console.debug no imprimen nada. Cada llamada añade una línea con formato a un búfer del motor. getConsoleLines() devuelve las líneas almacenadas como una lista de cadenas; getConsoleOutput() las devuelve unidas con \n, y clearConsole() vacía el búfer. El búfer no se vacía entre llamadas a 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

Establece un límite de recursos ajustable durante la ejecución. Hay exactamente uno:

Límite Predeterminado Descripción
maxLoopIterations 100000 número máximo de iteraciones por ejecución de un bucle for / while / do…while del intérprete que recorre el árbol
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Cualquier otro nombre se ignora sin aviso; no hay límites para la longitud de las cadenas, la salida de la consola ni el tiempo de ejecución.

El límite de bucle todavía no se aplica en todas partes. Los bucles que se ejecutan en la VM de bytecode, que abarca cuerpos de funciones y programas de nivel superior que acepta el compilador de bytecode, nunca se cuentan, así que while (true) {} se ejecuta hasta que PHP lo detiene (incidencia #19). No confíe en este límite para detener código no fiable; use PHP con max_execution_time o un proceso aparte.

Cuando se alcanza el límite, una expresión JS try / catch puede capturar el error (como un Error con el mensaje "Maximum loop iterations exceeded"). Si no se captura, llega a PHP como Phasis\Exceptions\InternalError, que extiende Phasis\Exceptions\RuntimeError, no JsThrowable.

El motor incorpora otros dos límites que no se pueden ajustar durante la ejecución: Phasis\Runtime\CallStack::maxDepth (4,096 marcos JS) y el límite de profundidad de cierres transpilados JsToPhp (8,192 marcos PHP). Al superarlos, ambos lanzan "Maximum call stack size exceeded", que también llega a PHP como InternalError. Estos límites están muy por encima de los valores de cualquier programa JS legítimo; aumentarlos requiere un cambio en el código, no un ajuste de configuración.

reset

PHP
public function reset(): void

Borra todas las variables globales definidas por el usuario y el búfer de la consola, y reconstruye la biblioteca estándar y el bucle de eventos.

En un motor creado en el modo diferido predeterminado, reset() elimina actualmente todas las funciones integradas instaladas de forma diferida: después, Map, JSON, Math, console, fetch y las demás quedan sin definir (incidencia #22). Hasta que se corrija, cree una instancia nueva de Engine o constrúyala con eager: true; en ese modo reset() funciona.

Bucle de eventos

El motor incluye un bucle de eventos JavaScript con setTimeout, setInterval, clearTimeout, clearInterval y queueMicrotask en el objeto global. Las microtareas (controladores de Promise, queueMicrotask) se agotan entre cada macrotarea; los temporizadores se activan según la fecha límite mediante una cola de tareas por ámbito.

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

eval() y evalAsModule() llaman automáticamente a runEventLoop() cuando termina el código síncrono. Así, un script que programa setTimeout(cb, 5000) en el nivel superior se ejecuta cb antes de que eval() devuelva el control: el bucle usa tiempo virtual y adelanta el reloj hasta el siguiente plazo pendiente en vez de esperar PHP. Coincide con la semántica de node script.js.

Para una integración en tiempo real (servidores de larga duración, integración con UI, programación según un reloj real), tickEventLoop() ejecuta una iteración (agota las microtareas y activa los temporizadores vencidos) y devuelve el control. pendingTaskCount() informa de los temporizadores pendientes; el entorno anfitrión puede gestionar su propio ciclo:

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

Un setInterval descontrolado tiene un límite: runEventLoop() se detiene tras 100,000 ciclos y lanza un \RuntimeException simple; el fallo en el código JS se muestra como un error PHP claro en vez de bloquear la ejecución. El intervalo sigue programado, así que todas las llamadas posteriores a eval() en el mismo motor lanzan el mismo error; descarte el motor después. Los errores no capturados en una función de temporizador se emiten mediante error_log() y el bucle sigue procesando tareas; un temporizador defectuoso no detiene el programa.

Hooks de Fetch

Phasis incluye fetch() activado de forma predeterminada y utiliza ext-curl de PHP como transporte subyacente. Tres hooks permiten al entorno anfitrión cambiar el transporte, controlar las solicitudes o activar el almacenamiento de cookies. Los tres aceptan una función PHP o un método de un objeto; consulte API web para conocer el formato de los valores.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Sustituye el CurlTransport predeterminado. La función recibe un descriptor de solicitud y un AbortSignal JsObject opcional; devuelve un descriptor de respuesta.

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 patrón habitual de integración: instalar un transporte simulado en las pruebas y el transporte curl real en producción.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Hook de política previa a la solicitud. Recibe el JsObject Request; puede lanzar una excepción para denegar, devolver un Request modificado para reescribir o devolver null/ningún valor para permitirla.

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

Almacén de cookies opcional. Acepta cualquier objeto que exponga get(string $url): string y set(string $url, string $header): void, tanto del lado de PHP como de 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());

Sin setCookieJar(), fetch ignora tanto las cabeceras Set-Cookie entrantes como las Cookie salientes: de forma predeterminada, no hay cookies.

Excepciones

Clase Extiende Cuándo
Phasis\Exceptions\JsThrowable RuntimeError un JS throw explícito que no se ha capturado
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError un error generado por el propio motor (por ejemplo, null.x o una variable no definida) que no se ha capturado
Phasis\Exceptions\InternalError RuntimeError se supera el límite de bucle o de pila de llamadas
Phasis\Exceptions\RuntimeError \RuntimeException clase base de las anteriores
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) el código fuente no se puede analizar
\RuntimeException execFile() no puede leer el archivo; el bucle de eventos alcanza su límite de 100,000 ciclos

Para capturar todos los errores de un script, capture Phasis\Exceptions\RuntimeError y Phasis\Exceptions\SyntaxError, o simplemente \RuntimeException.

Las excepciones lanzadas por sus propias funciones anfitrionas PHP son distintas: actualmente atraviesan JS try / catch y llegan sin cambios a quien llama a eval(), con su clase original (incidencia #20). Consulte Funciones anfitrionas.

Ciclo de vida

Engine se construye rápidamente (unos pocos ms en frío) y la biblioteca estándar se instala de forma diferida. Para la mayoría de los casos de integración, basta con un motor de larga duración por proceso. Para un uso multiinquilino, asigne a cada inquilino o solicitud su propio motor, para que no se filtren variables globales entre ellos. Actualmente, crear un motor nuevo es la forma fiable de empezar desde cero (consulte reset).

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