Vai al contenuto

Funzioni host

Qualsiasi closure o callable PHP fornito al motore tramite setGlobal() diventa una funzione callable JS. Non servono registrazioni manuali, schemi o marshalling: basta passare il callable.

Nozioni di base

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

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

Quando greet("world") viene eseguito in JS:

  1. La stringa "world" viene convertita da JS a PHP.
  2. La closure PHP viene invocata con l'argomento convertito.
  3. Il valore restituito viene riconvertito da PHP a JS.

La conversione segue le regole di conversione dei valori. Gli argomenti vengono copiati: un array o un oggetto JS arriva come array PHP e le modifiche apportate in PHP non cambiano il valore JS.

Numero di argomenti e parametri

Il motore passa al callable gli argomenti JS in ordine posizionale, così come sono, senza esaminare la firma. Le closure ordinarie ignorano gli argomenti aggiuntivi e i parametri variadici li raccolgono. Gli argomenti mancanti non vengono inseriti automaticamente: se JS passa meno argomenti del necessario, PHP genera ArgumentCountError. Assegnate valori predefiniti ai parametri facoltativi.

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

Conversione implicita dei tipi

Gli argomenti vengono convertiti in PHP secondo le regole JS→PHP. Il motore invoca la funzione richiamabile da codice che dichiara strict_types=1, quindi PHP verifica rigorosamente i tipi dei parametri scalari e non esegue conversioni implicite: un valore numerico JS passato a un parametro string è un TypeError. Come qualsiasi eccezione PHP proveniente da una funzione host, raggiunge il chiamante di eval() invece di JS (vedi sotto).

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

Esegui manualmente la conversione nella closure ((string) $value) oppure usa tipi unione (int|string).

Rilanciare in JS

Le normali eccezioni PHP lanciate all'interno di una funzione host non vengono attualmente convertite in errori JS. Attraversano tutti gli handler JS try / catch e .catch(), quindi raggiungono il codice PHP che ha chiamato eval() o call(), mantenendo la classe originale (problema n. 20). Tra queste ci sono \Exception, \RuntimeException, \DivisionByZeroError e gli errori \TypeError e \ArgumentCountError PHP generati per argomenti non validi.

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
}

Per generare un errore che il codice JS possa intercettare, lancia una delle classi di errore proprie del motore: Phasis\Exceptions\TypeError, RangeError o ReferenceError diventano in JS il tipo di errore JS corrispondente; qualsiasi altro Phasis\Exceptions\RuntimeError diventa un semplice Error, ciascuno con il tuo messaggio.

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
    }
');

Restituire valori complessi

Il valore restituito viene convertito secondo le regole PHP→JS. Per restituire un oggetto letterale JS, restituisci un 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);
');

Per restituire un array JS, restituisci un elenco sequenziale 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

Restituire un oggetto PHP, compresa una promise PHP da ReactPHP, Amp o Guzzle, produce JS undefined (vedi Conversione dei valori).

Funzioni host asincrone

Le funzioni host sono sincrone. Una promise PHP o un altro oggetto thenable non viene atteso: viene convertito in undefined, quindi await hostFn() si risolve in undefined. Esegui il lavoro in modo sincrono e restituisci il risultato; il codice JS può comunque await la chiamata, che racchiude il valore in una promise risolta:

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();
');

In particolare, per HTTP, il metodo integrato fetch() restituisce già una vera promise JS e può essere instradato tramite il tuo client PHP con un trasporto fetch personalizzato.

Note sulle prestazioni

Ogni chiamata a una funzione host attraversa una volta il confine PHP↔JS per gli argomenti e una volta per il valore restituito. Entrambe le conversioni copiano i dati, quindi hanno costo O(size of value): le primitive richiedono tempo costante, gli array e gli oggetti un tempo proporzionale alla loro dimensione.

Nei cicli interni più frequenti, preferisci una singola chiamata che svolga più lavoro a molte chiamate piccole ed evita di passare grandi array avanti e indietro quando cambiano solo pochi campi.