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
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Quando greet("world") viene eseguito in JS:
- La stringa
"world"viene convertita da JS a PHP. - La closure PHP viene invocata con l'argomento convertito.
- 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.
$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).
$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.
$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.
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:
$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:
$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:
$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.