API
Execuția API folosește o singură clasă: Phasis\Engine. Tot restul (JsValue, JsFunction, JsObject, …) este intern motorului și este expus doar funcțiilor gazdă care fac legătura între PHP↔JS. Singura altă interfață publică este parserul de sine stătător API (Phasis\Parser\Parser și clasele Phasis\Ast), care analizează sursa fără să o execute.
Construire
use Phasis\Engine;
$engine = new Engine(); // lazy mode (default)
$engine = new Engine(eager: true); // install every built-in up front
Un Engine nou expune biblioteca standard completă — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error, iar intrinsecurile limbajului de bază sunt instalate imediat, astfel încât specificația să funcționeze (nu puteți avea clase fără Function.prototype, async/await fără Promise sau literali regex fără RegExp). Toate celelalte — Math, JSON, Map, Set, Date, TypedArray și familia array-buffer, Atomics, Proxy, Reflect, console, Intl, familia weak-ref, Temporal, ShadowRealm, Web Platform Pack și Fetch Pack — sunt înregistrate ca accesor placeholder și materializate la prima citire. Citirea din global este transparentă: Map, typeof Map, 'Map' in globalThis, Object.keys(globalThis).includes('Map') funcționează, iar după prima citire placeholder-ul este înlocuit cu un descriptor de date obișnuit.
Transmiteți eager: true dacă aveți nevoie în mod special de comportamentul anterior încărcării leneșe — toate funcționalitățile încorporate sunt instalate la construire, iar întreaga suprafață a descriptorilor este vizibilă la introspecție. Harness-ul de conformitate (bin/test262, bin/wpt) construiește motoarele în modul eager, astfel încât test262/WPT să vadă formele descriptorilor așteptate.
eval
public function eval(string $source): mixed
Analizează și execută $source ca script clasic în domeniul global al motorului, apoi rulează bucla de evenimente până se golește. Returnează valoarea ultimei expresii evaluate, convertită în PHP (sau null dacă programul nu are o expresie finală). Consultați Conversia valorilor pentru reguli.
$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)
Un Promise nu este despachetat: eval('Promise.resolve(5)') returnează un tablou gol. Deoarece bucla de evenimente s-a golit până când se întoarce eval(), stocați valoarea stabilită într-un global și citiți-o cu un al doilea eval():
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out'); // 9
Un JS throw explicit pe care nu îl prinde nimic este expus ca Phasis\Exceptions\JsThrowable, al cărui mesaj este "Name: message" și a cărui proprietate $jsValue conține valoarea JsValue aruncată. Erorile generate de motor, precum citirea unei proprietăți a lui null sau folosirea unei variabile nedefinite, sunt expuse în schimb ca Phasis\Exceptions\TypeError, ReferenceError sau RangeError. Toate acestea extind Phasis\Exceptions\RuntimeError; erorile de analiză extind Phasis\Exceptions\SyntaxError. Consultați Excepții.
try {
$engine->eval('throw new Error("boom")');
} catch (\Phasis\Exceptions\JsThrowable $e) {
echo $e->getMessage();
// Error: boom
}
evalAsModule
public function evalAsModule(string $source, ?string $path = null): mixed
Evaluează $source ca modul ES: sunt disponibile import / export și await și import.meta la nivel superior, iar specificatorii relativi sunt rezolvați în raport cu $path (o cale absolută de fișier). Rulează bucla de evenimente ca eval() și returnează întotdeauna null; citiți rezultatele prin variabile globale sau din memoria tampon a consolei.
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
Încărcătorul rezolvă doar căi relative și absolute; consultați Limitări cunoscute.
execFile
public function execFile(string $path): mixed
Citește un fișier de pe disc și îi transmite conținutul către eval(), astfel încât să ruleze ca script clasic indiferent de extensie; un fișier cu import / export eșuează cu o eroare SyntaxError. Calea reală a fișierului devine baza pentru specificatorii import() dinamici. Folosiți evalAsModule() pentru fișiere modul.
$engine->execFile(__DIR__ . '/script.js');
Dacă fișierul nu poate fi citit, PHP emite un avertisment, iar execFile() aruncă o excepție \RuntimeException simplă („Cannot read file: …”).
setGlobal
public function setGlobal(string $name, mixed $value): void
Asociază o variabilă globală vizibilă în toate evaluările JS ulterioare. Valorile PHP sunt convertite în JS:
| Tipul PHP | Tipul JS |
|---|---|
null |
null |
bool |
boolean |
int / float |
number |
string |
string (chiar și când denumește o funcție, precum 'strlen') |
| tablou (listă) | Array (o copie) |
| tablou (asociativ) | Object (o copie) |
Closure, obiect apelabil, [$object, 'method'] |
function |
| orice alt obiect, generator sau resursă | undefined |
Tabelele sunt copiate: modificările pe care le face JS nu sunt vizibile în tabloul PHP. Obiectele simple PHP nu pot fi transmise încă (problema #18); consultați Obiecte partajate pentru soluții alternative.
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
În interiorul JS:
console.log(config.version); // "1.0"
log('hello'); // calls the PHP closure
call
public function call(string $name, mixed ...$args): mixed
Apelează o funcție globală JS cu argumente PHP (convertite ca pentru setGlobal). Valoarea returnată este convertită înapoi în PHP. Apelarea unui nume nedefinit aruncă Phasis\Exceptions\ReferenceError; apelarea unei variabile globale care nu este funcție aruncă Phasis\Exceptions\TypeError.
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3); // 5
Ieșirea consolei
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
console.log, console.info, console.warn, console.error și console.debug nu afișează nimic. Fiecare apel adaugă o linie formatată într-o memorie tampon a motorului. getConsoleLines() returnează liniile din memorie ca listă de șiruri, getConsoleOutput() le returnează unite cu \n, iar clearConsole() golește memoria tampon. Aceasta nu este golită între apelurile eval().
$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
public function setLimit(string $limit, int $value): void
Setează o limită de resurse configurabilă în timpul execuției. Există una singură:
| Limită | Implicită | Descriere |
|---|---|---|
maxLoopIterations |
100000 |
numărul maxim de iterații pentru fiecare rulare a buclei for / while / do…while de către interpretorul cu parcurgere arborescentă |
$engine->setLimit('maxLoopIterations', 1_000_000);
Orice alt nume este ignorat fără avertisment; nu există limite pentru lungimea șirurilor, ieșirea consolei sau timpul real de execuție.
Limita buclei nu este încă aplicată peste tot. Buclelele rulate pe mașina virtuală de cod octet, care acoperă corpurile funcțiilor și programele de nivel superior acceptate de compilatorul de cod octet, nu sunt numărate niciodată, așa că while (true) {} rulează până când PHP îl oprește (problema #19). Nu vă bazați pe aceasta pentru a opri codul care nu este de încredere; folosiți PHP max_execution_time sau un proces separat.
Când limita este atinsă, eroarea poate fi prinsă de un JS try / catch (ca Error cu mesajul „Maximum loop iterations exceeded”). Dacă nu o prinde nimic, ajunge la PHP ca Phasis\Exceptions\InternalError, care extinde Phasis\Exceptions\RuntimeError, nu JsThrowable.
Alte două limite maxime sunt încorporate în motor și nu pot fi configurate în timpul execuției: Phasis\Runtime\CallStack::maxDepth (4,096 cadre JS) și limita de adâncime pentru closure-urile transpuse JsToPhp (8,192 cadre PHP). Depășirea oricăreia aruncă „Maximum call stack size exceeded”, care ajunge tot ca InternalError la PHP. Aceste limite sunt mult peste necesarul oricărui program JS legitim — modificarea lor necesită schimbarea codului, nu a unei opțiuni de configurare.
reset
public function reset(): void
Șterge toate variabilele globale definite de utilizator și memoria tampon a consolei, apoi reconstruiește biblioteca standard și bucla de evenimente.
În prezent, pe un motor construit în modul leneș implicit, reset() pierde toate funcționalitățile încorporate instalate leneș: ulterior Map, JSON, Math, console, fetch și restul sunt nedefinite (problema #22). Până la remedierea problemei, construiți în schimb un Engine nou sau construiți cu eager: true, unde reset() funcționează.
Bucla de evenimente
Motorul include o buclă de evenimente JavaScript, cu setTimeout, setInterval, clearTimeout, clearInterval și queueMicrotask pe obiectul global. Microtask-urile (handler-ele Promise, queueMicrotask) sunt procesate între fiecare macrotask; temporizatoarele se declanșează în ordinea termenelor printr-o coadă de task-uri proprie fiecărui realm.
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
eval() și evalAsModule() apelează automat runEventLoop() după finalizarea codului sincron. Astfel, un script care programează setTimeout(cb, 5000) la nivel superior rulează cb înainte să se întoarcă eval() — bucla folosește timp virtual și își avansează ceasul la următorul termen în așteptare, fără să aștepte PHP. Respectă semantica node script.js.
Pentru integrarea în timp real (servere cu durată lungă de funcționare, integrare UI, programare după un ceas real), tickEventLoop() rulează o iterație (procesează microtask-urile și declanșează temporizatoarele ajunse la termen), apoi returnează. pendingTaskCount() raportează temporizatoarele restante; aplicația gazdă își poate rula propria buclă:
while ($engine->pendingTaskCount() > 0) {
$engine->tickEventLoop();
usleep(1000);
}
Un setInterval care nu se oprește este limitat: runEventLoop() se oprește la 100,000 de cicluri și aruncă o excepție simplă \RuntimeException — eroarea din codul JS este semnalată clar ca eroare PHP, în loc să blocheze execuția. Intervalul rămâne programat, așa că fiecare apel ulterior la eval() pe același motor aruncă aceeași eroare; renunțați apoi la motor. Erorile neprinse din callback-urile temporizatoarelor sunt emise prin error_log(), iar bucla continuă să proceseze task-urile; un temporizator defect nu oprește programul.
Hook-uri Fetch
Phasis include fetch() activat implicit, cu ext-curl din PHP ca transport de bază. Trei hook-uri permit aplicației gazdă să schimbe transportul, să controleze solicitările sau să activeze stocarea cookie-urilor. Toate acceptă fie un callable PHP, fie o metodă a unui obiect; consultați API-uri web pentru contractul privind forma valorilor.
setFetchTransport
public function setFetchTransport(callable $transport): void
Înlocuiește CurlTransport implicit. Callable-ul primește un descriptor de solicitare și un JsObject AbortSignal opțional; returnează un descriptor de răspuns.
// 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 tipar obișnuit de integrare: instalați un transport simulat în teste și transportul curl real în producție.
setFetchPolicy
public function setFetchPolicy(callable $hook): void
Hook de politică înaintea solicitării. Primește JsObject-ul Request; poate arunca o excepție pentru a o refuza, poate returna un Request modificat pentru a o rescrie sau poate returna null/nicio valoare pentru a o permite.
// 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
public function setCookieJar(mixed $jar): void
Stocare opțională pentru cookie-uri. Acceptă orice obiect care expune get(string $url): string și set(string $url, string $header): void — pe partea PHP sau pe partea JS.
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());
Fără setCookieJar(), fetch ignoră atât antetele Set-Cookie de intrare, cât și antetele Cookie de ieșire — implicit, nu sunt folosite cookie-uri.
Excepții
| Clasă | Extinde | Când |
|---|---|---|
Phasis\Exceptions\JsThrowable |
RuntimeError |
un JS throw explicit pe care nu l-a prins nimic |
Phasis\Exceptions\TypeError, ReferenceError, RangeError |
RuntimeError |
o eroare generată de motor (de exemplu, null.x sau o variabilă nedefinită) pe care nu a prins-o nimic |
Phasis\Exceptions\InternalError |
RuntimeError |
limita buclei sau limita stivei de apeluri a fost depășită |
Phasis\Exceptions\RuntimeError |
\RuntimeException |
clasa de bază a celor de mai sus |
Phasis\Parser\ParseError |
Phasis\Exceptions\SyntaxError (\RuntimeException) |
sursa nu poate fi analizată |
\RuntimeException |
execFile() nu poate citi fișierul; bucla de evenimente a atins limita de 100,000 cicluri |
Pentru a prinde toate erorile unui script, prindeți Phasis\Exceptions\RuntimeError și Phasis\Exceptions\SyntaxError sau folosiți pur și simplu \RuntimeException.
Excepțiile aruncate de propriile funcții gazdă PHP sunt diferite: în prezent trec prin JS try / catch și ajung neschimbate la apelantul lui eval(), păstrându-și clasa inițială (problema #20). Consultați Funcții gazdă.
Ciclul de viață
Engine se construiește rapid (~câteva ms la rece), iar biblioteca standard se instalează leneș. Pentru majoritatea scenariilor de integrare, este potrivit un singur motor cu durată lungă de funcționare per proces. Pentru utilizarea cu mai mulți clienți, alocați fiecărui client sau fiecărei solicitări propriul motor, astfel încât variabilele globale să nu se transfere între ele. În prezent, construirea unui motor nou este metoda sigură de a porni de la zero (consultați reset).
foreach ($requests as $req) {
$engine = new Engine();
$engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
$engine->execFile($req->scriptPath);
}