
# 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](https://phasis.inline0.dev/es/docs/parser) (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](https://phasis.inline0.dev/es/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 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](#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
```

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](https://phasis.inline0.dev/es/docs/compatibility/limitations#module-loader).

## 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](https://github.com/inline0/phasis/issues/18)); consulte [Objetos compartidos](https://phasis.inline0.dev/es/docs/interop/shared-objects) 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](https://github.com/inline0/phasis/issues/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](https://github.com/inline0/phasis/issues/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](https://phasis.inline0.dev/es/docs/compatibility/web-apis) 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](https://github.com/inline0/phasis/issues/20)). Consulte [Funciones anfitrionas](https://phasis.inline0.dev/es/docs/interop/host-functions#throwing-back-into-js).

## 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](#reset)).

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