Pular para o conteúdo

Primeiros passos

Instalação

Shell
composer require phasis/phasis

Phasis requer PHP 8.2 ou posterior com três extensões, todas exigidas por composer.json:

  • mbstring — tratamento de strings.
  • bcmath — operações aritméticas com BigInt e tratamento de números com precisão inteira.
  • curl — o transporte padrão por trás de fetch().

mbstring e curl vêm habilitados na maioria das compilações. Muitas vezes, bcmath não vem: as imagens oficiais de php Docker (php:8.x-cli, php:8.x-fpm) não o incluem, e algumas distribuições o oferecem em um pacote separado (php-bcmath). Sem ele, Composer se recusa a instalar Phasis. Em um Dockerfile baseado na imagem oficial, adicione:

Dockerfile
RUN docker-php-ext-install bcmath

Execute php -m para ver quais extensões sua compilação tem.

ext-intl é opcional. Phasis funciona sem ela, mas toda a família Intl.* (Collator, NumberFormat, DateTimeFormat, PluralRules, Locale, DisplayNames, ListFormat, RelativeTimeFormat, Segmenter, DurationFormat) e os calendários não ISO Temporal (hebrew, islamic, japanese, persian etc.) precisam dela. Instale-a sempre que o aplicativo host oferecer JS que envolva formatação sensível à localidade. ext-openssl também é opcional: a criptografia AES-CBC, AES-CTR e AES-GCM de crypto.subtle precisa dela, mas digests e HMAC funcionam sem ela.

Sem exec, sem FFI, sem Node.js, sem extensão nativa personalizada.

Executar um script a partir do CLI

Shell
./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

O CLI exibe o que o script escreve com console.log; não exibe o valor da última expressão.

Consulte a Referência do CLI para ver a lista completa de comandos.

Incorporar em PHP

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

A instância de Engine mantém seu estado global entre chamadas, então as chamadas subsequentes de eval() veem tudo o que você definiu anteriormente.

console.log e seus métodos irmãos acrescentam conteúdo a um buffer na engine, em vez de escreverem em stdout. Leia-o com getConsoleOutput() (uma string, com linhas unidas por \n) ou getConsoleLines() (uma lista de linhas), e esvazie-o com clearConsole().

A inicialização a frio é rápida: somente o núcleo necessário para carregar a especificação (Object, Function, Array, Promise, Symbol, BigInt, RegExp e os tipos de erro) é instalado de imediato. Map, TypedArray, Temporal, Intl, URL, fetch, os pacotes Web Platform / Fetch completos e todo o restante são materializados de forma transparente na primeira leitura. Passe new Engine(eager: true) para instalar todos os recursos integrados durante a construção — útil para ferramentas de conformidade que inspecionam descritores antes de lê-los; raramente necessário na incorporação normal.

Chamar funções de JS a partir de PHP

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

call() procura a função pelo nome no objeto global e a invoca com os argumentos PHP fornecidos (convertidos automaticamente em valores JS).

Expor uma função chamável de PHP como função JS

PHP
$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")');

As closures de PHP são encapsuladas como funções JS. Seus argumentos são convertidos de JS para PHP na chamada, e seus valores de retorno são convertidos de PHP de volta para JS.

Objetos PHP

Objetos PHP ainda não podem ser compartilhados com JavaScript. setGlobal() com um objeto PHP simples vincula undefined (acompanhado em problema #18). Passe os dados como arrays, que são copiados para JS, e exponha o comportamento como closures:

PHP
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

Consulte Objetos compartilhados para ver os detalhes e outras soluções alternativas.

Limites de recursos

PHP
$engine = new Engine();
$engine->setLimit('maxLoopIterations', 100_000);

maxLoopIterations (limite padrão 100,000) é o único limite ajustável durante a execução e, atualmente, só se aplica ao código executado pelo interpretador que percorre a árvore. Loops executados pela VM de bytecode (corpos de funções e a maior parte do código de nível superior) não são contabilizados, então o limite por si só não interrompe while (true) {} (problema #19). Quando o limite é atingido, o erro pode ser capturado em JS; se não for capturado, chega a PHP como Phasis\Exceptions\InternalError, uma subclasse de Phasis\Exceptions\RuntimeError.

O engine também tem um limite fixo de pilha de chamadas de 4,096 quadros (e uma proteção de 8,192 quadros para closures transpiladas); ultrapassá-lo gera "Maximum call stack size exceeded", novamente como InternalError. Não há limite de tempo de parede, então execute código não confiável no próprio PHP max_execution_time ou em um processo separado. Consulte Referência: APIsetLimit.

Próximas etapas