Primeros pasos
Instalación
composer require phasis/phasis
Phasis requiere PHP 8.2 o posterior con tres extensiones, todas requeridas por composer.json:
mbstring— gestión de cadenas.bcmath— aritmética BigInt y gestión de números con precisión entera.curl— el transporte predeterminado detrás defetch().
mbstring y curl están habilitadas en la mayoría de las compilaciones. bcmath suele faltar: las imágenes oficiales de php Docker (php:8.x-cli, php:8.x-fpm) no la incluyen, y algunas distribuciones la ofrecen por separado (php-bcmath). Sin ella, Composer no puede instalar Phasis. En un Dockerfile basado en la imagen oficial, añade:
RUN docker-php-ext-install bcmath
Ejecuta php -m para ver qué extensiones incluye tu compilación.
ext-intl es opcional. Phasis funciona sin ella, pero toda la familia Intl.* (Collator, NumberFormat, DateTimeFormat, PluralRules, Locale, DisplayNames, ListFormat, RelativeTimeFormat, Segmenter, DurationFormat) y los calendarios de Temporal que no son ISO (hebrew, islamic, japanese, persian, etc.) la requieren. Instálala cuando la aplicación anfitriona ofrezca JS que utilicen formato sensible a la configuración regional. ext-openssl también es opcional: la necesitan crypto.subtle cifrado AES-CBC, AES-CTR y AES-GCM; los resúmenes y HMAC funcionan sin ella.
Sin exec, sin FFI, sin Node.js y sin extensiones nativas personalizadas.
Ejecutar un script desde CLI
./vendor/bin/phasis -e 'console.log(1 + 2 * 3)'
# 7
./vendor/bin/phasis -e 'console.log([1, 2, 3].map(x => x * x).join(","))'
# 1,4,9
./vendor/bin/phasis path/to/script.js
CLI imprime lo que el script escribe con console.log; no imprime el valor de la última expresión.
Consulta la referencia de CLI para ver todos los comandos disponibles.
Incrustar en PHP
<?php
require __DIR__ . '/vendor/autoload.php';
use Phasis\Engine;
$engine = new Engine();
// Evaluate an expression
$result = $engine->eval('1 + 2 * 3');
echo $result;
// 7
// Execute a script file
$engine->execFile('path/to/script.js');
// Bridge a PHP value into the JS global scope
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->eval('console.log(config.version)');
// console output is buffered on the engine, not printed
echo $engine->getConsoleOutput();
// 1.0
La instancia de Engine conserva su estado global entre llamadas, así que las llamadas posteriores a eval() ven todo lo que se haya definido antes.
console.log y sus funciones relacionadas añaden datos a un búfer del motor en lugar de escribir en stdout. Léelo con getConsoleOutput() (una cadena, con las líneas unidas por \n) o con getConsoleLines() (una lista de líneas), y vacíalo con clearConsole().
El arranque en frío es rápido: al principio solo se instala el núcleo necesario para cargar la especificación (Object, Function, Array, Promise, Symbol, BigInt, RegExp y los tipos de error). Map, TypedArray, Temporal, Intl, URL, fetch, los paquetes completos Web Platform y Fetch, y todo lo demás se materializan de forma transparente en la primera lectura. Pasa new Engine(eager: true) para instalar todos los elementos integrados al construir la instancia; resulta útil en herramientas de conformidad que inspeccionan descriptores antes de leerlos y rara vez hace falta en una integración normal.
Llamar funciones de JS desde PHP
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);
// 5
call() busca la función por nombre en el objeto global y la invoca con los argumentos PHP proporcionados (convertidos automáticamente en valores JS).
Exponer una función invocable de PHP como función de JS
$engine->setGlobal('fetchData', function (string $url): array {
return json_decode(file_get_contents($url), true);
});
$engine->eval('const users = fetchData("https://api.example.com/users")');
Las clausuras de PHP se envuelven como funciones de JS. Al llamarlas, sus argumentos se convierten de JS a PHP, y sus valores de retorno se convierten de PHP a JS.
Objetos PHP
Los objetos PHP aún no se pueden compartir con JavaScript. setGlobal() con un objeto PHP sencillo vincula undefined (seguimiento en el problema n.º 18). Pasa los datos como matrices, que se copian en JS, y expón el comportamiento como clausuras:
class Counter {
public int $value = 0;
public function increment(): void { $this->value++; }
}
$counter = new Counter();
$engine->setGlobal('increment', fn () => $counter->increment());
$engine->eval('increment(); increment();');
echo $counter->value;
// 2
Consulta Objetos compartidos para ver los detalles y otras soluciones alternativas.
Límites de recursos
$engine = new Engine();
$engine->setLimit('maxLoopIterations', 100_000);
maxLoopIterations (valor predeterminado 100,000) es el único límite que se puede ajustar durante la ejecución y, actualmente, solo se aplica al código ejecutado por el intérprete que recorre el árbol. No se cuentan los bucles ejecutados por la VM de bytecode (los cuerpos de funciones y la mayor parte del código de nivel superior), así que el límite por sí solo no detiene while (true) {} (problema n.º 19). Al alcanzar el límite, el error se puede capturar en JS; si no se captura, llega a PHP como Phasis\Exceptions\InternalError, una subclase de Phasis\Exceptions\RuntimeError.
El motor también tiene un límite fijo de pila de llamadas de 4,096 marcos (y una protección de 8,192 marcos para clausuras transpiladas); superarlo genera el error "Maximum call stack size exceeded", también como InternalError. No hay límite de tiempo real, así que ejecuta el código no fiable bajo PHP, con su propio max_execution_time, o en un proceso separado. Consulta API: setLimit.
Pasos siguientes
- Referencia de API — API completa de
Phasis\Engine. - Interoperabilidad — reglas de conversión de valores, funciones del anfitrión y objetos PHP.
- Compatibilidad — funciones implementadas y cobertura de test262.
- Analizador y Formateador — analiza y formatea JavaScript sin ejecutarlo.
- Avanzado — arquitectura, VM de bytecode, pruebas de rendimiento y de referencia.