API
Vykonávacie API tvorí jedna trieda: Phasis\Engine. Všetko ostatné (JsValue, JsFunction, JsObject, …) je interné pre engine a sprístupňuje sa len hostiteľským funkciám, ktoré prepájajú PHP↔JS. Jediným ďalším verejným rozhraním je samostatný parser API (Phasis\Parser\Parser a triedy Phasis\Ast), ktorý parsuje zdrojový kód bez jeho vykonania.
Vytvorenie
use Phasis\Engine;
$engine = new Engine(); // lazy mode (default)
$engine = new Engine(eager: true); // install every built-in up front
Čerstvá inštancia Engine sprístupňuje celú štandardnú knižnicu — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error a základné jazykové intrinzické funkcie sa inštalujú okamžite, aby fungovala špecifikácia (bez Function.prototype nemôžete mať triedy, bez Promise async/await a bez RegExp regulárne výrazy v literáloch). Všetko ostatné — Math, JSON, Map, Set, Date, TypedArray a rodina array-buffer, Atomics, Proxy, Reflect, console, Intl, rodina weak-ref, Temporal, ShadowRealm, Web Platform Pack a Fetch Pack — sa registruje ako zástupný prístupový prvok a vytvorí sa až pri prvom čítaní. Čítanie globálnej premennej je transparentné: Map, typeof Map, 'Map' in globalThis, Object.keys(globalThis).includes('Map') fungujú a po prvom čítaní sa zástupný prvok nahradí bežným opisovačom dátovej vlastnosti.
Ak potrebujete správanie spred zavedenia lenivého načítavania, zadajte eager: true — všetky vstavané funkcie sa nainštalujú pri vytvorení a ich úplné deskriptory sú dostupné na introspekciu. Testovací rámec zhody (bin/test262, bin/wpt) vytvára enginy v režime eager, aby test262/WPT videli očakávané tvary deskriptorov.
eval
public function eval(string $source): mixed
Parsuje a vykoná $source ako klasický skript v globálnom rozsahu enginu a potom spúšťa event loop, kým sa nevyprázdni. Vráti hodnotu posledného vyhodnoteného výrazu prevedenú na PHP (alebo null, ak program nemal záverečný výraz). Pravidlá nájdete v časti Prevod hodnôt.
$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)
Promise sa nerozbaľuje: eval('Promise.resolve(5)') vráti prázdne pole. Keďže event loop je vyčerpaný v čase, keď sa eval() vráti, uložte vyriešenú hodnotu do globálnej premennej a načítajte ju druhým volaním eval():
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out'); // 9
Ak explicitné JS throw nič nezachytí, prejaví sa ako Phasis\Exceptions\JsThrowable, ktorého správa je "Name: message" a vlastnosť $jsValue obsahuje vyhodenú hodnotu JsValue. Chyby, ktoré vyvolá samotný engine, napríklad čítanie vlastnosti hodnoty null alebo nedefinovanej premennej, sa namiesto toho prejavia ako Phasis\Exceptions\TypeError, ReferenceError alebo RangeError. Všetky tieto triedy dedia od Phasis\Exceptions\RuntimeError; chyby parsovania dedia od Phasis\Exceptions\SyntaxError. Pozrite si Výnimky.
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
Vyhodnotí $source ako modul ES: k dispozícii sú import / export a najvyššia úroveň await a import.meta; relatívne špecifikátory sa rozlišujú voči $path (absolútnej ceste k súboru). Event loop sa spúšťa ako pri eval() a vždy sa vráti null; výsledky čítajte z globálnych premenných alebo vyrovnávacej pamäte konzoly.
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
Načítavač rozlišuje iba relatívne a absolútne cesty; pozrite si Známe obmedzenia.
execFile
public function execFile(string $path): mixed
Načíta súbor z disku a odovzdá jeho obsah do eval(), takže sa spustí ako klasický skript bez ohľadu na príponu; súbor s import / export zlyhá s chybou SyntaxError. Skutočná cesta k súboru sa stane základom pre dynamické špecifikátory import(). Pre súbory modulov použite evalAsModule().
$engine->execFile(__DIR__ . '/script.js');
Ak sa súbor nedá prečítať, PHP vydá varovanie a execFile() vyvolá obyčajnú chybu \RuntimeException ("Cannot read file: …").
setGlobal
public function setGlobal(string $name, mixed $value): void
Naviaže globálnu premennú viditeľnú vo všetkých nasledujúcich vyhodnoteniach JS. Hodnoty PHP sa prevedú na JS:
| typ PHP | typ JS |
|---|---|
null |
null |
bool |
boolean |
int / float |
number |
string |
string (aj keď pomenúva funkciu, napríklad 'strlen') |
| pole (zoznam) | Array (kópia) |
| pole (asociatívne) | Object (kópia) |
Closure, volateľný objekt, [$object, 'method'] |
function |
| akýkoľvek iný objekt, generátor alebo prostriedok | undefined |
Polia sa kopírujú: zmeny, ktoré v nich vykoná JS, sa v poli PHP neprejavia. Jednoduché objekty PHP zatiaľ nemožno odovzdať (issue #18); alternatívne riešenia nájdete v časti Zdieľané objekty.
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
V rámci JS:
console.log(config.version); // "1.0"
log('hello'); // calls the PHP closure
call
public function call(string $name, mixed ...$args): mixed
Zavolá globálnu funkciu JS s argumentmi PHP (prevedenými rovnako ako pri setGlobal). Návratová hodnota sa prevedie späť na PHP. Volanie nedefinovaného názvu vyvolá Phasis\Exceptions\ReferenceError; volanie globálnej premennej, ktorá nie je funkciou, vyvolá Phasis\Exceptions\TypeError.
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3); // 5
Výstup konzoly
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
console.log, console.info, console.warn, console.error a console.debug nič nevypisujú. Každé volanie pridá do vyrovnávacej pamäte enginu jeden naformátovaný riadok. getConsoleLines() vráti uložené riadky ako zoznam reťazcov, getConsoleOutput() ich vráti spojené pomocou \n a clearConsole() vyprázdni pamäť. Pamäť sa medzi volaniami eval() nevymaže.
$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
Nastaví limit zdrojov, ktorý možno meniť za behu. Existuje presne jeden:
| Limit | Predvolená hodnota | Opis |
|---|---|---|
maxLoopIterations |
100000 |
maximálny počet iterácií na jedno spustenie slučky for / while / do…while v interpreteri so stromovým prechodom |
$engine->setLimit('maxLoopIterations', 1_000_000);
Každý iný názov sa potichu ignoruje; neexistujú limity dĺžky reťazca, výstupu konzoly ani času behu.
Limit slučky sa zatiaľ nevynucuje všade. Počet iterácií slučiek vykonávaných vo VM bajtkódu, ktorý pokrýva telá funkcií a programy najvyššej úrovne prijaté kompilátorom bajtkódu, sa nikdy nesleduje, takže while (true) {} beží, kým ho nezastaví PHP (issue #19). Nespoliehajte sa naň pri zastavovaní nedôveryhodného kódu; použite PHP s max_execution_time alebo samostatný proces.
Po dosiahnutí limitu môže chybu zachytiť blok JS try / catch (ako Error so správou "Maximum loop iterations exceeded"). Ak ju nič nezachytí, dostane sa do PHP ako Phasis\Exceptions\InternalError, ktorý dedí od Phasis\Exceptions\RuntimeError, nie od JsThrowable.
V engine sú pevne nastavené aj dva ďalšie limity, ktoré nemožno meniť za behu: Phasis\Runtime\CallStack::maxDepth (4,096 rámcov JS) a ochranný limit hĺbky transpilovaných uzáverov JsToPhp (8,192 rámcov PHP). Pri prekročení oba vyvolajú "Maximum call stack size exceeded", ktorá sa prejaví aj v PHP ako InternalError. Tieto limity sú výrazne vyššie než pri akomkoľvek legitímnom programe JS — ich zvýšenie si vyžaduje zmenu kódu, nie nastavenia.
reset
public function reset(): void
Vymaže všetky globálne premenné definované používateľom a vyrovnávaciu pamäť konzoly a znova vytvorí štandardnú knižnicu aj event loop.
V engine vytvorenom v predvolenom lenivom režime reset() momentálne odstráni všetky lenivo nainštalované vstavané funkcie: potom sú Map, JSON, Math, console, fetch a ostatné nedefinované (issue #22). Kým sa to neopraví, vytvorte radšej novú inštanciu Engine alebo ju vytvorte s eager: true, kde reset() funguje.
Event loop
Engine obsahuje vlastný event loop JavaScript s setTimeout, setInterval, clearTimeout, clearInterval a queueMicrotask na globálnom objekte. Mikrotasky (obslužné rutiny Promise, queueMicrotask) sa vyprázdnia medzi každou makroúlohou; časovače sa spúšťajú v poradí termínov pomocou frontu úloh pre každú realm.
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
eval() a evalAsModule() automaticky zavolajú runEventLoop() po dokončení synchrónneho kódu. Skript, ktorý naplánuje setTimeout(cb, 5000) na najvyššej úrovni, teda spustí cb ešte pred návratom eval() — loop používa virtuálny čas a posunie hodiny na najbližší čakajúci termín namiesto PHP. Zodpovedá sémantike node script.js.
Pri integrácii v reálnom čase (dlho bežiace servery, integrácia UI, plánovanie podľa skutočných hodín) spustí tickEventLoop() jednu iteráciu (vyprázdni mikrotasky, spustí všetky časovače, ktorých termín nastal) a vráti sa. pendingTaskCount() oznamuje zostávajúce časovače; integrátor si môže vytvoriť vlastný cyklus:
while ($engine->pendingTaskCount() > 0) {
$engine->tickEventLoop();
usleep(1000);
}
Nekonečne bežiaci setInterval má stanovený limit: runEventLoop() sa zastaví po 100,000 cykloch a vyvolá obyčajnú chybu \RuntimeException — chyba v kóde JS sa prejaví ako zrozumiteľná chyba PHP namiesto zablokovania. Interval zostáva naplánovaný, takže každé ďalšie volanie eval() v tom istom engine vyvolá rovnakú chybu; engine potom zahoďte. Nezachytené chyby v spätnom volaní časovača sa odošlú cez error_log() a loop pokračuje vo vyprázdňovaní frontu; jedna chybná úloha nezastaví program.
Háčiky Fetch
Phasis štandardne obsahuje zapnutú funkciu fetch(), ktorá ako transport používa ext-curl z PHP. Tri háčiky umožňujú nahradiť transport, filtrovať požiadavky alebo zapnúť ukladanie súborov cookie. Každý prijíma volateľný prvok PHP alebo metódu objektu; pravidlá tvaru hodnôt nájdete v časti Web API.
setFetchTransport
public function setFetchTransport(callable $transport): void
Nahradí predvolený CurlTransport. Volateľný prvok prijíma opisovač požiadavky a voliteľný AbortSignal JsObject; vracia opisovač odpovede.
// 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(),
];
});
Bežný vzor integrácie: v testoch nainštalovať falošný transport a v produkcii skutočný curl transport.
setFetchPolicy
public function setFetchPolicy(callable $hook): void
Háčik politiky pred odoslaním požiadavky. Prijíma Request JsObject; odmietne ju vyhodením výnimky, prepíše vrátením upraveného Request alebo povolí vrátením null či bez hodnoty.
// 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
Voliteľné úložisko súborov cookie. Prijíma ľubovoľný objekt so get(string $url): string a set(string $url, string $header): void — na strane PHP alebo 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());
Bez setCookieJar() fetch ignoruje prichádzajúce hlavičky Set-Cookie aj odchádzajúce hlavičky Cookie — predvolene sa nepoužívajú žiadne súbory cookie.
Výnimky
| Trieda | Dedi od | Kedy |
|---|---|---|
Phasis\Exceptions\JsThrowable |
RuntimeError |
explicitné JS throw, ktoré nič nezachytilo |
Phasis\Exceptions\TypeError, ReferenceError, RangeError |
RuntimeError |
chyba vyvolaná samotným enginom (napríklad null.x alebo nedefinovaná premenná), ktorú nič nezachytilo |
Phasis\Exceptions\InternalError |
RuntimeError |
bol prekročený limit slučky alebo limit zásobníka volaní |
Phasis\Exceptions\RuntimeError |
\RuntimeException |
základná trieda vyššie uvedených tried |
Phasis\Parser\ParseError |
Phasis\Exceptions\SyntaxError (\RuntimeException) |
zdrojový kód sa nedá parsovať |
\RuntimeException |
execFile() nedokáže prečítať súbor; event loop dosiahol limit 100,000 cyklov |
Ak chcete zachytiť každú chybu skriptu, zachyťte Phasis\Exceptions\RuntimeError a Phasis\Exceptions\SyntaxError alebo jednoducho \RuntimeException.
Výnimky vyvolané vo vlastných hostiteľských funkciách PHP sú odlišné: v súčasnosti prechádzajú cez JS try / catch a dostanú sa nezmenené k volajúcemu eval(), pričom si zachovajú pôvodnú triedu (problém č. 20). Pozrite si Hostiteľské funkcie.
Životný cyklus
Vytvorenie Engine je nenáročné (za studena trvá približne niekoľko ms) a štandardná knižnica sa načítava až na požiadanie. Pri väčšine scenárov vloženia stačí jeden dlho žijúci engine na proces. Pri použití s viacerými nájomníkmi vytvorte pre každého nájomníka alebo požiadavku vlastný engine, aby medzi nimi neunikali globálne premenné. Vytvorenie nového enginu je v súčasnosti spoľahlivý spôsob, ako začať s čistým stavom (pozrite si reset).
foreach ($requests as $req) {
$engine = new Engine();
$engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
$engine->execFile($req->scriptPath);
}