Vai al contenuto

API

L’API di esecuzione è una classe: Phasis\Engine. Tutto il resto (JsValue, JsFunction, JsObject, …) è interno al motore ed è esposto solo alle funzioni host che fanno da ponte tra PHP↔JS. L’unica altra interfaccia pubblica è il parser autonomo API (le classi Phasis\Parser\Parser e Phasis\Ast), che analizza il codice sorgente senza eseguirlo.

Creazione

PHP
use Phasis\Engine;

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

Una nuova istanza di Engine espone la libreria standard completa: Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error e le primitive del linguaggio principale vengono installate subito, per garantire il funzionamento della specifica (senza Function.prototype non si possono avere classi, senza Promise async/await e senza RegExp i letterali regex). Tutto il resto — Math, JSON, Map, Set, Date, TypedArray e la famiglia degli array buffer, Atomics, Proxy, Reflect, console, Intl, la famiglia weak-ref, Temporal, ShadowRealm, il Web Platform Pack e il Fetch Pack — viene registrato come accessor segnaposto e materializzato alla prima lettura. L’accesso alle variabili globali è trasparente: Map, typeof Map, 'Map' in globalThis e Object.keys(globalThis).includes('Map') funzionano tutti; dopo la prima lettura, il segnaposto viene sostituito da un normale descrittore dati.

Passate eager: true se vi serve espressamente il comportamento precedente al caricamento lazy: ogni funzione integrata viene installata alla creazione, con l’intera superficie del descrittore disponibile per l’introspezione. Il test harness di conformità (bin/test262, bin/wpt) crea gli engine in modalità eager, affinché test262/WPT rilevi le forme dei descrittori previste.

eval

PHP
public function eval(string $source): mixed

Analizza ed esegue $source come script classico nell’ambito globale dell’engine, poi esegue il ciclo degli eventi finché non è vuoto. Restituisce il valore dell’ultima espressione valutata, convertito in PHP (oppure null se il programma non termina con un’espressione). Per le regole, vedi Conversione dei valori.

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)

Una Promise non viene risolta automaticamente: eval('Promise.resolve(5)') restituisce un array vuoto. Poiché il ciclo degli eventi si è svuotato quando eval() restituisce, memorizzate il valore risolto in una variabile globale e leggetelo con una seconda chiamata a eval():

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

Un JS throw esplicito non intercettato viene restituito come Phasis\Exceptions\JsThrowable, il cui messaggio è "Name: message" e la cui proprietà $jsValue contiene il valore JsValue lanciato. Gli errori generati direttamente dall’engine, per esempio accedendo a una proprietà di null o a una variabile non definita, vengono restituiti invece come Phasis\Exceptions\TypeError, ReferenceError o RangeError. Tutti questi estendono Phasis\Exceptions\RuntimeError; gli errori di analisi estendono Phasis\Exceptions\SyntaxError. Vedi Eccezioni.

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

Valuta $source come modulo ES: sono disponibili import / export e le istruzioni await e import.meta a livello superiore; gli identificatori relativi vengono risolti rispetto a $path (un percorso assoluto al file). Esegue il ciclo degli eventi come eval() e restituisce sempre null; leggete i risultati tramite le variabili globali o il buffer della console.

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

Il caricatore risolve solo percorsi relativi e assoluti; vedi Limitazioni note.

execFile

PHP
public function execFile(string $path): mixed

Legge un file dal disco e ne passa il contenuto a eval(), che lo esegue come script classico a prescindere dall’estensione; un file con import / export genera un SyntaxError. Il percorso reale del file diventa la base per gli identificatori import() dinamici. Per i file di modulo, usate evalAsModule().

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

Se non è possibile leggere il file, PHP emette un avviso e execFile() lancia un semplice \RuntimeException ("Impossibile leggere il file: …").

setGlobal

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

Associa una variabile globale, visibile in tutte le successive valutazioni di JS. I valori PHP vengono convertiti in JS:

Tipo PHP Tipo JS
null null
bool boolean
int / float number
string string (anche quando indica una funzione, come 'strlen')
array (lista) Array (una copia)
array (associativo) Object (una copia)
Closure, oggetto invocabile, [$object, 'method'] function
qualsiasi altro oggetto, generatore o risorsa undefined

Gli array vengono copiati: le modifiche apportate da JS non sono visibili nell’array PHP. Per ora non è possibile passare oggetti semplici PHP (problema n. 18); per le alternative, vedi Oggetti condivisi.

PHP
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));

All’interno di JS:

JavaScript
console.log(config.version);   // "1.0"
log('hello');                  // calls the PHP closure

call

PHP
public function call(string $name, mixed ...$args): mixed

Chiama una funzione globale JS con argomenti PHP (convertiti come per setGlobal). Il valore restituito viene riconvertito in PHP. Chiamare un nome non definito genera Phasis\Exceptions\ReferenceError; chiamare una variabile globale che non è una funzione genera Phasis\Exceptions\TypeError.

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

Output della 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 non stampano a schermo. Ogni chiamata aggiunge una riga formattata a un buffer dell’engine. getConsoleLines() restituisce le righe memorizzate come elenco di stringhe, getConsoleOutput() le restituisce unite con \n e clearConsole() svuota il buffer. Il buffer non viene cancellato tra una chiamata eval() e l’altra.

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

Imposta un limite alle risorse modificabile a runtime. È disponibile esattamente un limite:

Limite Predefinito Descrizione
maxLoopIterations 100000 numero massimo di iterazioni per ogni esecuzione di un ciclo for / while / do…while gestito dall'interprete tree-walking
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Qualsiasi altro nome viene ignorato senza avvisi; non ci sono limiti alla lunghezza delle stringhe, all'output della console o al tempo di esecuzione.

Il limite dei cicli non è ancora applicato ovunque. I cicli eseguiti dalla VM a bytecode, che comprende i corpi delle funzioni e i programmi di primo livello accettati dal compilatore bytecode, non vengono mai conteggiati, quindi while (true) {} continua a essere eseguito finché PHP non lo interrompe (issue #19). Non affidatevi a questo limite per fermare codice non attendibile: usate PHP's max_execution_time oppure un processo separato.

Quando si raggiunge il limite, l'errore può essere intercettato da un JS try / catch (come Error con il messaggio "Maximum loop iterations exceeded"). Se non viene intercettato, raggiunge PHP come Phasis\Exceptions\InternalError, che estende Phasis\Exceptions\RuntimeError, non JsThrowable.

Altri due limiti sono integrati nel motore e non sono modificabili a runtime: Phasis\Runtime\CallStack::maxDepth (4,096 frame JS) e il limite di profondità per le closure transpilate da JsToPhp (8,192 frame PHP). Se superati, entrambi generano "Maximum call stack size exceeded", che raggiunge anche PHP come InternalError. Sono molto al di sopra di qualsiasi programma JS legittimo: aumentarli richiede una modifica al codice, non un'opzione di configurazione.

reset

PHP
public function reset(): void

Cancella tutte le variabili globali definite dall'utente e il buffer della console, quindi ricrea la libreria standard e il ciclo di eventi.

Su un motore creato in modalità lazy predefinita, al momento reset() rimuove tutte le funzioni integrate installate pigramente: in seguito, Map, JSON, Math, console, fetch e le altre risultano undefined (issue #22). Finché il problema non viene risolto, create invece un nuovo Engine oppure istanziatelo con eager: true, modalità in cui reset() funziona.

Ciclo di eventi

Il motore include un ciclo di eventi JavaScript con setTimeout, setInterval, clearTimeout, clearInterval e queueMicrotask sull'oggetto globale. Le microtask (gestori Promise, queueMicrotask) vengono eseguite tra ogni macrotask; i timer scattano in ordine di scadenza tramite una coda di attività specifica per realm.

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

eval() e evalAsModule() chiamano automaticamente runEventLoop() al termine del codice sincrono. Quindi uno script che pianifica setTimeout(cb, 5000) al livello superiore viene eseguito cb prima che eval() restituisca il controllo: il ciclo usa il tempo virtuale e porta l'orologio alla prossima scadenza in sospeso, senza attendere PHP. Conforme alla semantica di node script.js.

Per l'integrazione in tempo reale (server di lunga durata, integrazione UI, pianificazione basata su un orologio reale), tickEventLoop() esegue una iterazione (svuota le microtask, attiva gli eventuali timer scaduti) e restituisce il controllo. pendingTaskCount() segnala i timer ancora in sospeso; chi integra il motore può gestire il proprio ciclo:

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

Un setInterval senza fine è limitato: runEventLoop() si arresta dopo 100,000 cicli e genera un semplice \RuntimeException; l'errore nel codice JS viene segnalato chiaramente come errore PHP anziché bloccare l'esecuzione. L'intervallo resta pianificato, quindi ogni successivo eval() sullo stesso motore genera lo stesso errore; in seguito, scartate il motore. Gli errori non intercettati nei callback dei timer vengono emessi tramite error_log() e il ciclo continua; un timer difettoso non interrompe il programma.

Hook Fetch

Phasis include fetch() attivo per impostazione predefinita, con ext-curl di PHP come trasporto sottostante. Tre hook consentono di sostituire il trasporto, filtrare le richieste o attivare la memorizzazione dei cookie. Tutti e tre accettano una funzione PHP oppure un metodo di un oggetto; per il contratto sui formati dei valori, consultate Web APIs.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Sostituisce il CurlTransport predefinito. La funzione riceve un descrittore della richiesta e un AbortSignal JsObject facoltativo; restituisce un descrittore della risposta.

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

Un modello comune di integrazione: installare un trasporto simulato nei test e il trasporto curl reale in produzione.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Hook per i criteri di verifica preliminare. Riceve il Request JsObject; può generare un errore per negare la richiesta, restituire un Request modificato per riscriverla oppure restituire null/nessun valore per consentirla.

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

Archivio cookie facoltativo. Accetta qualsiasi oggetto che esponga get(string $url): string e set(string $url, string $header): void, lato PHP o 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());

Senza setCookieJar(), fetch ignora sia gli header Set-Cookie in ingresso sia quelli Cookie in uscita: per impostazione predefinita i cookie sono disattivati.

Eccezioni

Classe Estende Quando
Phasis\Exceptions\JsThrowable RuntimeError viene generato un JS throw esplicito che non viene intercettato
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError errore generato dal motore (ad esempio null.x o una variabile non definita) che non viene intercettato
Phasis\Exceptions\InternalError RuntimeError viene superato il limite dei cicli o il limite dello stack di chiamate
Phasis\Exceptions\RuntimeError \RuntimeException classe base delle precedenti
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) il sorgente non può essere analizzato
\RuntimeException execFile() non riesce a leggere il file; il ciclo di eventi ha raggiunto il limite di 100,000 cicli

Per intercettare tutti gli errori di uno script, intercettate Phasis\Exceptions\RuntimeError e Phasis\Exceptions\SyntaxError oppure semplicemente \RuntimeException.

Le eccezioni generate dalle vostre funzioni host PHP sono diverse: al momento passano attraverso JS try / catch e raggiungono invariato il chiamante di eval(), mantenendo la classe originale (issue #20). Consultate Funzioni host.

Ciclo di vita

Engine si crea rapidamente (pochi ms a freddo) e installa pigramente la libreria standard. Nella maggior parte degli scenari di integrazione è sufficiente un unico motore di lunga durata per processo. Per l'uso multi-tenant, assegnate a ogni tenant o richiesta un motore proprio, così le variabili globali non si condividono. Creare un nuovo motore è oggi il modo più affidabile per ripartire da uno stato pulito (vedi reset).

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