Pular para o conteúdo

Funções do host

Qualquer closure ou função chamável PHP passada ao mecanismo por meio de setGlobal() torna-se uma função JS chamável. Não é necessário registrá-la manualmente nem definir um esquema ou fazer marshalling — basta passar a função.

Noções básicas

PHP
$engine->setGlobal('greet', function (string $name): string {
    return "Hello, $name!";
});

echo $engine->eval('greet("world")');
// Hello, world!

Quando greet("world") é executado em JS:

  1. A string "world" é convertida de JS para PHP.
  2. A closure PHP é chamada com o argumento convertido.
  3. O valor retornado é convertido de PHP para JS.

A conversão segue as regras de conversão de valores. Os argumentos são cópias: um array ou objeto JS chega como array PHP, e alterá-lo em PHP não altera o valor JS.

Aridade e parâmetros

O mecanismo passa os argumentos JS à função chamável na ordem posicional, sem modificá-los; ele não inspeciona a assinatura. Closures comuns ignoram argumentos extras, enquanto parâmetros variádicos os recebem. Argumentos ausentes não são preenchidos: se JS passar menos argumentos do que a função exige, PHP lança ArgumentCountError. Defina valores padrão para parâmetros opcionais.

PHP
$engine->setGlobal('format', function (string $template, ...$args): string {
    return vsprintf($template, $args);
});

echo $engine->eval('format("%s is %d years old", "Ada", 36)');
// Ada is 36 years old

Conversão implícita de tipos

Os argumentos são convertidos para PHP usando as regras de JS→PHP. O mecanismo chama seu callable a partir de código que declara strict_types=1, então PHP verifica estritamente os tipos dos parâmetros escalares e não faz coerção: um número JS passado a um parâmetro string é um TypeError. Como qualquer exceção PHP de uma função hospedeira, ela chega ao chamador de eval() em vez de JS (veja abaixo).

PHP
$engine->setGlobal('len', fn(string $s): int => strlen($s));

$engine->eval('len(42)');  // PHP TypeError: Argument #1 ($s) must be of type string, int given

Faça a coerção manualmente dentro da closure ((string) $value) ou use tipos de união (int|string).

Propagando exceções de volta para JS

As exceções comuns de PHP lançadas dentro de uma função hospedeira não são convertidas em erros JS atualmente. Elas passam por todos os manipuladores JS try / catch e .catch() e chegam ao código PHP que chamou eval() ou call(), mantendo sua classe original (problema #20). Isso inclui \Exception, \RuntimeException, \DivisionByZeroError e os erros \TypeError e \ArgumentCountError PHP lançados para argumentos inválidos.

PHP
$engine->setGlobal('divide', function (int $a, int $b): int {
    if ($b === 0) {
        throw new \DivisionByZeroError('cannot divide by zero');
    }
    return intdiv($a, $b);
});

try {
    $engine->eval('
        try { divide(10, 0); } catch (e) { /* never runs */ }
    ');
} catch (\DivisionByZeroError $e) {
    echo $e->getMessage();  // cannot divide by zero
}

Para lançar um erro que o código JS possa capturar, lance uma das classes de erro do próprio mecanismo: Phasis\Exceptions\TypeError, RangeError ou ReferenceError chegam a JS como o tipo de erro JS correspondente, e qualquer outro Phasis\Exceptions\RuntimeError chega como um Error simples, ambos com sua mensagem.

PHP
use Phasis\Exceptions\RangeError;

$engine->setGlobal('divide', function (int $a, int $b): int {
    if ($b === 0) {
        throw new RangeError('cannot divide by zero');
    }
    return intdiv($a, $b);
});

$engine->eval('
    try {
        divide(10, 0);
    } catch (e) {
        console.log(e.name, e.message);  // RangeError cannot divide by zero
    }
');

Retorno de valores complexos

O valor de retorno é convertido segundo as regras de PHP→JS. Para retornar um literal de objeto JS, retorne um array associativo PHP:

PHP
$engine->setGlobal('lookup', function (string $key): array {
    return ['key' => $key, 'value' => 42, 'meta' => ['ts' => time()]];
});

$engine->eval('
    const r = lookup("answer");
    console.log(r.value, r.meta.ts);
');

Para retornar um array JS, retorne uma lista sequencial PHP:

PHP
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));

$engine->eval('range(1, 5).reduce((a, b) => a + b)');  // 15

Retornar um objeto PHP, incluindo uma promise PHP de ReactPHP, Amp ou Guzzle, resulta em JS undefined (veja Conversão de valores).

Funções assíncronas do hospedeiro

As funções hospedeiras são síncronas. Uma promise PHP ou outro objeto thenable não é aguardado: ele é convertido em undefined, então await hostFn() é resolvido como undefined. Faça o trabalho de forma síncrona e retorne o resultado; o código JS ainda pode await a chamada, que envolve o valor em uma promise resolvida:

PHP
$engine->setGlobal('loadBody', function (string $url): string {
    return file_get_contents($url);
});

$engine->eval('
    async function main() {
        const body = await loadBody("https://example.com");
        console.log(body.length);
    }
    main();
');

Especificamente para HTTP, o fetch() integrado já retorna uma promise JS real e pode ser encaminhado pelo seu próprio cliente PHP com um transporte fetch personalizado.

Observações sobre desempenho

Cada chamada de função hospedeira cruza a fronteira PHP↔JS uma vez para os argumentos e outra para o valor de retorno. Ambas as conversões fazem cópias, portanto, têm custo O(size of value) — operações com valores primitivos têm tempo constante; arrays e objetos têm custo proporcional ao tamanho.

Em loops internos críticos para o desempenho, prefira uma chamada que faça mais trabalho a várias chamadas pequenas e evite passar arrays grandes de um lado para outro quando apenas alguns campos mudam.