Pular para o conteúdo

API

O ponto de entrada de execução API consiste em uma única classe: Phasis\Engine. Todo o restante (JsValue, JsFunction, JsObject, …) é interno ao mecanismo e só fica exposto às funções hospedeiras que conectam PHP↔JS. A única outra interface pública é o parserAPI independente (Phasis\Parser\Parser e as classes Phasis\Ast), que analisa o código-fonte sem executá-lo.

Construção

PHP
use Phasis\Engine;

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

Um Engine recém-criado expõe a biblioteca padrão completa — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error e as intrínsecas da linguagem principal são instaladas imediatamente para que a especificação funcione (não há classes sem Function.prototype, async/await sem Promise nem literais de regex sem RegExp). Todo o restante — Math, JSON, Map, Set, Date, TypedArray e a família de array buffers, Atomics, Proxy, Reflect, console, Intl, a família weak-ref, Temporal, ShadowRealm, o Web Platform Pack e o Fetch Pack — é registrado como acessor substituto e materializado na primeira leitura. A leitura do global é transparente: Map, typeof Map, 'Map' in globalThis e Object.keys(globalThis).includes('Map') funcionam, e após a primeira leitura o substituto é trocado por um descritor de dados normal.

Passe eager: true se precisar especificamente do comportamento anterior ao carregamento tardio — todos os recursos integrados são instalados na construção e a superfície completa dos descritores fica visível à introspecção. O harness de conformidade (bin/test262, bin/wpt) cria mecanismos no modo imediato para que test262/WPT vejam as formas de descritor esperadas.

eval

PHP
public function eval(string $source): mixed

Analisa e executa $source como um script clássico no escopo global do mecanismo e, em seguida, executa o loop de eventos até esvaziá-lo. Retorna o valor da última expressão avaliada, convertido para PHP (ou null se o programa não tiver uma expressão final). Consulte Conversão de valores para ver as regras.

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)

Uma Promise não é desembrulhada: eval('Promise.resolve(5)') retorna um array vazio. Como o loop de eventos já foi esvaziado quando eval() retorna, armazene o valor resolvido em um global e leia-o com uma segunda chamada a eval():

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

Uma exceção JS throw explícita que não é capturada aparece como Phasis\Exceptions\JsThrowable, cuja mensagem é "Name: message" e cuja propriedade $jsValue contém o valor JsValue lançado. Erros gerados pelo próprio mecanismo, como acessar uma propriedade de null ou usar uma variável indefinida, aparecem como Phasis\Exceptions\TypeError, ReferenceError ou RangeError. Todos eles estendem Phasis\Exceptions\RuntimeError; erros de análise sintática estendem Phasis\Exceptions\SyntaxError. Consulte Exceções.

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

Avalia $source como um módulo ES: import / export, await e import.meta no nível superior ficam disponíveis, e os especificadores relativos são resolvidos com base em $path (um caminho absoluto de arquivo). Executa o loop de eventos como eval() e sempre retorna null; leia os resultados por meio dos globais ou do buffer do console.

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

O carregador resolve apenas caminhos relativos e absolutos; consulte Limitações conhecidas.

execFile

PHP
public function execFile(string $path): mixed

Lê um arquivo do disco e passa seu conteúdo para eval(), que o executa como um script clássico, independentemente da extensão; um arquivo com import / export gera um SyntaxError. O caminho real do arquivo se torna a base para especificadores dinâmicos de import(). Use evalAsModule() para arquivos de módulo.

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

Se não for possível ler o arquivo, PHP emite um aviso e execFile() lança um \RuntimeException simples ("Não foi possível ler o arquivo: …").

setGlobal

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

Vincula uma variável global visível em todas as avaliações subsequentes de JS. Os valores PHP são convertidos em JS:

tipo PHP tipo JS
null null
bool boolean
int / float number
string string (mesmo quando nomeia uma função, como 'strlen')
array (lista) Array (uma cópia)
array (associativo) Object (uma cópia)
Closure, objeto invocável, [$object, 'method'] function
qualquer outro objeto, gerador ou recurso undefined

Os arrays são copiados: as alterações que JS fizer neles não aparecem no array PHP. Ainda não é possível passar objetos simples de PHP (problema #18); consulte Objetos compartilhados para ver 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

Chama uma função global de JS com argumentos PHP (convertidos como em setGlobal). O valor retornado é convertido novamente para PHP. Chamar um nome não definido gera Phasis\Exceptions\ReferenceError; chamar uma variável global que não seja uma função gera Phasis\Exceptions\TypeError.

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

Saída do console

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

console.log, console.info, console.warn, console.error e console.debug não imprimem nada. Cada chamada acrescenta uma linha formatada a um buffer no mecanismo. getConsoleLines() retorna as linhas armazenadas como uma lista de strings, getConsoleOutput() as retorna unidas por \n e clearConsole() esvazia o buffer. O buffer não é limpo entre chamadas 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

Define um limite de recursos ajustável durante a execução. Há exatamente um:

Limite Padrão Descrição
maxLoopIterations 100000 máximo de iterações por execução de loop for / while / do…while pelo interpretador que percorre a árvore
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Qualquer outro nome é ignorado silenciosamente; não há limites para o comprimento das strings, a saída do console ou o tempo de execução.

O limite de loops ainda não é aplicado em todos os lugares. Loops executados na VM de bytecode, que cobre corpos de funções e programas de nível superior aceitos pelo compilador de bytecode, nunca são contabilizados; assim, while (true) {} é executado até que PHP o interrompa (problema #19). Não confie nesse limite para interromper código não confiável; use PHP max_execution_time ou um processo separado.

Ao atingir o limite, o erro pode ser capturado por um JS try / catch (como um Error com a mensagem "Máximo de iterações de loop excedido"). Se nada o capturar, chega a PHP como Phasis\Exceptions\InternalError, que estende Phasis\Exceptions\RuntimeError, não JsThrowable.

Dois outros limites máximos estão embutidos no mecanismo e não podem ser ajustados durante a execução: Phasis\Runtime\CallStack::maxDepth (quadros 4,096 JS) e o limite de profundidade de closures transpiladas por JsToPhp (quadros 8,192 PHP). Ultrapassar qualquer um deles gera "Tamanho máximo da pilha de chamadas excedido", que também chega a PHP como InternalError. Esses limites ficam bem acima do necessário para qualquer programa JS legítimo — aumentá-los exige uma alteração no código, não um ajuste de configuração.

reset

PHP
public function reset(): void

Remove todas as variáveis globais definidas pelo usuário e o buffer do console e recria a biblioteca padrão e o loop de eventos.

Em um mecanismo criado no modo tardio padrão, reset() atualmente perde todos os recursos integrados instalados sob demanda: depois disso, Map, JSON, Math, console, fetch e os demais ficam indefinidos (problema #22). Até que isso seja corrigido, crie um novo Engine ou use eager: true na criação; assim, reset() funciona.

Loop de eventos

O mecanismo inclui um JavaScript loop de eventos com setTimeout, setInterval, clearTimeout, clearInterval e queueMicrotask no objeto global. As microtarefas (handlers de Promise, queueMicrotask) são processadas entre cada macrotarefa; os temporizadores disparam por ordem de prazo usando uma fila de tarefas por realm.

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

eval() e evalAsModule() chamam runEventLoop() automaticamente quando o código síncrono termina. Assim, um script que agenda setTimeout(cb, 5000) no nível superior executa cb antes de eval() retornar — o loop usa tempo virtual, avançando o relógio até o próximo prazo pendente em vez de aguardar PHP. Corresponde à semântica de node script.js.

Para incorporação em tempo real (servidores de longa duração, integração com UI, agendamento com base em um relógio real), tickEventLoop() executa uma iteração (processa microtarefas, dispara os temporizadores vencidos) e retorna. pendingTaskCount() informa os temporizadores pendentes; o incorporador pode executar seu próprio loop:

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

Um setInterval descontrolado é limitado: runEventLoop() para após 100,000 ciclos e lança um \RuntimeException simples — a falha no código JS aparece como um erro PHP claro, em vez de travar. O intervalo continua agendado, então cada chamada posterior a eval() no mesmo mecanismo lança o mesmo erro; descarte o mecanismo depois disso. Erros não capturados em um callback de temporizador são emitidos por error_log(), e o loop continua processando tarefas; um temporizador com erro não encerra o programa.

Hooks de Fetch

Phasis inclui fetch() ativado por padrão, usando ext-curl de PHP como transporte subjacente. Três hooks permitem que o incorporador troque o transporte, controle as solicitações ou ative o armazenamento de cookies. Os três aceitam um callable PHP ou um método de um objeto; consulte APIs da Web para conhecer o contrato do formato dos valores.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Substitui o CurlTransport padrão. O callable recebe um descritor de solicitação e um AbortSignal JsObject opcional; retorna um descritor de resposta.

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

Um padrão comum de incorporação: instalar um transporte simulado nos testes e o transporte curl real em produção.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Hook de política prévia à solicitação. Recebe o Request JsObject; pode lançar uma exceção para negar, retornar um Request modificado para reescrever ou retornar null/nenhum valor para permitir.

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

Armazenamento de cookies opcional. Aceita qualquer objeto que exponha get(string $url): string e set(string $url, string $header): void — do lado de PHP ou 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());

Sem setCookieJar(), fetch ignora tanto os cabeçalhos Set-Cookie de entrada quanto os cabeçalhos Cookie de saída — o padrão é sem cookies.

Exceções

Classe Estende Quando
Phasis\Exceptions\JsThrowable RuntimeError uma JS throw explícita que nada capturou
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError um erro gerado pelo próprio mecanismo (por exemplo, null.x ou uma variável indefinida) que nada capturou
Phasis\Exceptions\InternalError RuntimeError o limite do loop ou o limite da pilha de chamadas foi ultrapassado
Phasis\Exceptions\RuntimeError \RuntimeException classe base das classes acima
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) o código-fonte não pode ser analisado
\RuntimeException execFile() não consegue ler o arquivo; o loop de eventos atingiu seu limite de 100,000 ciclos

Para capturar toda falha de um script, capture Phasis\Exceptions\RuntimeError e Phasis\Exceptions\SyntaxError ou simplesmente \RuntimeException.

As exceções lançadas pelas suas próprias funções host de PHP são diferentes: atualmente, elas passam por JS try / catch e chegam inalteradas ao chamador de eval(), com a classe original (problema #20). Consulte Funções host.

Ciclo de vida

Engine é barato de construir (alguns ms em uma inicialização fria) e a biblioteca padrão é carregada de forma preguiçosa. Para a maioria dos cenários de incorporação, basta um mecanismo de longa duração por processo. Em ambientes multi-inquilino, dê a cada inquilino ou solicitação seu próprio mecanismo para que os globais nunca vazem entre eles. Hoje, construir um mecanismo novo é a forma confiável de começar com um estado limpo (consulte redefinição).

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