API
Vmesnik za izvajanje API predstavlja en sam razred: Phasis\Engine. Vse drugo (JsValue, JsFunction, JsObject, …) je notranji del pogona in je na voljo samo gostiteljskim funkcijam, ki povezujejo PHP↔JS. Edini drug javni vmesnik je samostojni razčlenjevalnik parser API (Phasis\Parser\Parser in razredi Phasis\Ast), ki razčleni izvorno kodo, ne da bi jo izvedel.
Ustvarjanje
use Phasis\Engine;
$engine = new Engine(); // lazy mode (default)
$engine = new Engine(eager: true); // install every built-in up front
Svež Engine razkrije celotno standardno knjižnico — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error in osnovne jezikovne intrinzične funkcije se namestijo takoj, da specifikacija deluje (razredov ni mogoče uporabljati brez Function.prototype, async/await brez Promise, regularnih izrazov v literalih pa brez RegExp). Vse drugo — Math, JSON, Map, Set, Date, TypedArray in družina medpomnilnikov polj, Atomics, Proxy, Reflect, console, Intl, družina šibkih referenc, Temporal, ShadowRealm, Web Platform Pack in Fetch Pack — se registrira kot nadomestni dostopnik in ustvari ob prvem branju. Branje globalne spremenljivke je pregledno: Map, typeof Map, 'Map' in globalThis, Object.keys(globalThis).includes('Map') vse deluje, po prvem branju pa se nadomestni dostopnik zamenja z običajnim deskriptorjem podatkov.
Podajte eager: true, če potrebujete vedenje pred uvedbo odloženega nalaganja — vse vgrajene funkcije so nameščene že ob ustvarjanju, introspekcija pa vidi celoten nabor deskriptorjev. Testno okolje za skladnost (bin/test262, bin/wpt) ustvarja pogone v takojšnjem načinu, da test262/WPT vidita pričakovane oblike deskriptorjev.
eval
public function eval(string $source): mixed
Razčleni in izvede $source kot klasični skript v globalnem obsegu pogona, nato pa izvaja zanko dogodkov, dokler se ne izprazni. Vrne vrednost zadnjega ovrednotenega izraza, pretvorjeno v PHP (ali null, če program nima zaključnega izraza). Pravila so opisana v razdelku Pretvorba vrednosti.
$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)
Obljuba se ne razreši samodejno: eval('Promise.resolve(5)') vrne prazno polje. Ker je zanka dogodkov izpraznjena, ko se eval() vrne, shranite razrešeno vrednost v globalno spremenljivko in jo preberite z drugim klicem eval():
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out'); // 9
Izrecni JS throw, ki ga nič ne prestreže, se prikaže kot Phasis\Exceptions\JsThrowable; njegovo sporočilo je "Name: message", njegova lastnost $jsValue pa vsebuje sproženi JsValue. Napake, ki jih sproži sam pogon, na primer branje lastnosti vrednosti null ali nedefinirane spremenljivke, se namesto tega prikažejo kot Phasis\Exceptions\TypeError, ReferenceError ali RangeError. Vse te napake dedujejo od Phasis\Exceptions\RuntimeError; napake pri razčlenjevanju dedujejo od Phasis\Exceptions\SyntaxError. Glejte razdelek Izjeme.
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
Ovrednoti $source kot modul ES: na voljo sta import / export, deklaraciji await in import.meta na vrhnji ravni, relativni označevalniki pa se razrešijo glede na $path (absolutno pot do datoteke). Zanko dogodkov izvede tako kot eval() in vedno vrne null; rezultate preberite prek globalnih spremenljivk ali medpomnilnika konzole.
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
Nalagalnik razreši samo relativne in absolutne poti; glejte Znane omejitve.
execFile
public function execFile(string $path): mixed
Prebere datoteko z diska in njeno vsebino posreduje funkciji eval(), zato se izvede kot klasični skript ne glede na končnico; datoteka z import / export povzroči SyntaxError. Dejanska pot do datoteke postane osnova za dinamične označevalnike import(). Za datoteke modulov uporabite evalAsModule().
$engine->execFile(__DIR__ . '/script.js');
Če datoteke ni mogoče prebrati, PHP izpiše opozorilo, execFile() pa sproži navadni \RuntimeException (»Cannot read file: …«).
setGlobal
public function setGlobal(string $name, mixed $value): void
Veže globalno spremenljivko, ki je vidna vsem poznejšim ovrednotenjem JS. Vrednosti PHP se pretvorijo v JS:
| Vrsta PHP | Vrsta JS |
|---|---|
null |
null |
bool |
boolean |
int / float |
number |
string |
string (tudi kadar poimenuje funkcijo, kot je 'strlen') |
| polje (seznam) | Array (kopija) |
| polje (asociativno) | Object (kopija) |
Closure, klicljiv objekt, [$object, 'method'] |
function |
| kateri koli drug objekt, generator ali vir | undefined |
Polja se kopirajo: spremembe, ki jih v njih naredi JS, niso vidne v polju PHP. Navadnih objektov PHP še ni mogoče posredovati (težava št. 18); za rešitve glejte razdelek Skupni objekti.
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
Znotraj JS:
console.log(config.version); // "1.0"
log('hello'); // calls the PHP closure
call
public function call(string $name, mixed ...$args): mixed
Pokliče globalno funkcijo JS z argumenti PHP (pretvorjenimi enako kot pri setGlobal). Vrnjena vrednost se pretvori nazaj v PHP. Klic nedefiniranega imena sproži Phasis\Exceptions\ReferenceError; klic globalne spremenljivke, ki ni funkcija, pa sproži Phasis\Exceptions\TypeError.
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3); // 5
Izpis konzole
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
console.log, console.info, console.warn, console.error in console.debug ne izpisujejo ničesar. Vsak klic doda eno oblikovano vrstico v medpomnilnik pogona. getConsoleLines() vrne shranjene vrstice kot seznam nizov, getConsoleOutput() jih vrne združene z \n, clearConsole() pa izprazni medpomnilnik. Med klici eval() se medpomnilnik ne izbriš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
Nastavi omejitev virov, ki jo je mogoče spreminjati med izvajanjem. Obstaja samo ena:
| Omejitev | Privzeto | Opis |
|---|---|---|
maxLoopIterations |
100000 |
največje število ponovitev na zagon zanke for / while / do…while v tolmaču, ki prehaja drevo |
$engine->setLimit('maxLoopIterations', 1_000_000);
Vsako drugo ime se tiho prezre; omejitev za dolžino nizov, izpis konzole ali čas izvajanja ni.
Omejitev zank še ni uveljavljena povsod. Zanke, ki se izvajajo v navideznem stroju bajtne kode — ta zajema telesa funkcij in programe na vrhnji ravni, ki jih sprejme prevajalnik bajtne kode — se nikoli ne štejejo, zato se while (true) {} izvaja, dokler ga PHP ne ustavi (težava št. 19). Ne zanašajte se na to, da bo omejitev ustavila nezaupanja vredno kodo; uporabite PHP max_execution_time ali ločen proces.
Ko je omejitev dosežena, lahko napako prestreže blok JS try / catch (kot Error s sporočilom »Maximum loop iterations exceeded«). Če je nič ne prestreže, doseže PHP kot Phasis\Exceptions\InternalError, ki deduje od Phasis\Exceptions\RuntimeError, ne od JsThrowable.
V pogon sta vgrajeni še dve omejitvi, ki ju ni mogoče spreminjati med izvajanjem: Phasis\Runtime\CallStack::maxDepth (okviri 4,096 JS) in omejitev globine transpilanih zaprtij JsToPhp (okviri 8,192 PHP). Pri prekoračitvi obe sprožita »Maximum call stack size exceeded«, kar se prav tako prikaže v PHP kot InternalError. Njuni vrednosti sta precej višji od tistih, ki jih potrebuje kateri koli običajen program JS — povečati ju je mogoče s spremembo kode, ne nastavitve.
reset
public function reset(): void
Izbriše vse uporabniško določene globalne spremenljivke in medpomnilnik konzole ter znova ustvari standardno knjižnico in zanko dogodkov.
Pri pogonu, ustvarjenem v privzetem odloženem načinu, reset() trenutno odstrani vse vgrajene funkcije, nameščene z odloženim nalaganjem: nato Map, JSON, Math, console, fetch in vse druge niso definirane (težava št. 22). Dokler težava ni odpravljena, raje ustvarite nov Engine ali pa ga ustvarite z eager: true, kjer reset() deluje.
Zanka dogodkov
Pogon vključuje zanko dogodkov JavaScript z globalnim objektom, ki vsebuje setTimeout, setInterval, clearTimeout, clearInterval in queueMicrotask. Mikroopravila (obravnavalniki Promise, queueMicrotask) se izpraznijo po vsakem makroopravilu; časovniki se prožijo po vrstnem redu rokov prek čakalne vrste opravil posameznega področja.
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
eval() in evalAsModule() samodejno pokličeta runEventLoop() po zaključku sinhrone kode. Skript, ki na vrhnji ravni načrtuje setTimeout(cb, 5000), se zato izvede cb, preden se eval() vrne — zanka uporablja navidezni čas in uro premakne na naslednji čakajoči rok, namesto da bi čakala PHP. Ustreza semantiki node script.js.
Za vdelavo v sisteme z realnim časom (dolgotrajni strežniki, integracija UI, načrtovanje glede na dejansko uro) tickEventLoop() izvede eno iteracijo (izprazni mikroopravila, sproži zapadle časovnike) in se vrne. pendingTaskCount() poroča o čakajočih časovnikih; gostiteljska aplikacija lahko upravlja lastno zanko:
while ($engine->pendingTaskCount() > 0) {
$engine->tickEventLoop();
usleep(1000);
}
Neskončno izvajanje setInterval je omejeno: runEventLoop() se ustavi pri 100,000 ciklih in sproži navadni \RuntimeException — napaka v kodi JS se jasno prikaže kot napaka PHP, namesto da bi se izvajanje zataknilo. Interval ostane načrtovan, zato vsak naslednji eval() v istem pogonu sproži isto napako; pogon zatem zavrzite. Neprestrežene napake v povratnem klicu časovnika se oddajo prek error_log(), zanka pa nadaljuje izvajanje; ena napačna funkcija časovnika ne ustavi programa.
Kljuke za Fetch
Phasis privzeto vključuje fetch(), ki kot osnovni prenosni mehanizem uporablja ext-curl iz PHP. Tri kljuke gostiteljski aplikaciji omogočajo zamenjavo prenosnega mehanizma, omejevanje zahtevkov ali vklop shranjevanja piškotkov. Vsaka sprejme klicni objekt PHP ali metodo objekta; opis oblike vrednosti je v razdelku Spletni API-ji.
setFetchTransport
public function setFetchTransport(callable $transport): void
Zamenja privzeti CurlTransport. Klicni objekt prejme opis zahtevka in izbirni AbortSignal JsObject; vrne opis odgovora.
// 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(),
];
});
Pogost vzorec vdelave: v testih namestite ponarejeni prenosni mehanizem, v produkciji pa pravi prenosni mehanizem curl.
setFetchPolicy
public function setFetchPolicy(callable $hook): void
Kljuka pravilnika pred pošiljanjem zahtevka. Prejme Request JsObject; lahko sproži izjemo za zavrnitev, vrne spremenjeni Request za preoblikovanje ali vrne null/ne vrne vrednosti, da zahtevek dovoli.
// 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
Shramba piškotkov, ki jo je treba izrecno vklopiti. Sprejme kateri koli objekt z metodama get(string $url): string in set(string $url, string $header): void — na strani PHP ali 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());
Brez setCookieJar() fetch prezre tako dohodne glave Set-Cookie kot odhodne glave Cookie — privzeto se piškotki ne uporabljajo.
Izjeme
| Razred | Deduje od | Kdaj |
|---|---|---|
Phasis\Exceptions\JsThrowable |
RuntimeError |
izrecni JS throw, ki ga nič ne prestreže |
Phasis\Exceptions\TypeError, ReferenceError, RangeError |
RuntimeError |
napaka, ki jo sproži sam pogon (npr. null.x ali nedefinirana spremenljivka) in je nič ne prestreže |
Phasis\Exceptions\InternalError |
RuntimeError |
presežena je omejitev zanke ali velikosti sklada klicev |
Phasis\Exceptions\RuntimeError |
\RuntimeException |
osnovni razred za zgornje razrede |
Phasis\Parser\ParseError |
Phasis\Exceptions\SyntaxError (\RuntimeException) |
izvorne kode ni mogoče razčleniti |
\RuntimeException |
execFile() ne more prebrati datoteke; zanka dogodkov je dosegla omejitev 100,000 ciklov |
Če želite prestreči vse napake skripta, prestrezite Phasis\Exceptions\RuntimeError in Phasis\Exceptions\SyntaxError ali preprosto \RuntimeException.
Izjeme, ki jih sprožijo lastne gostiteljske funkcije PHP, so drugačne: trenutno se nespremenjene prebijejo skozi JS try / catch do klicatelja funkcije eval(), ohranijo pa svoj izvirni razred (težava št. 20). Glejte razdelek Gostiteljske funkcije.
Življenjski cikel
Engine je poceni za ustvarjanje (nekaj ms pri hladnem zagonu), standardna knjižnica pa se namešča z odloženim nalaganjem. Za večino vdelav je primeren en dolgotrajno delujoč pogon na proces. Pri uporabi za več najemnikov vsakemu najemniku ali zahtevku dodelite lasten pogon, da se globalne spremenljivke med njimi ne prenašajo. Zanesljiv način za čist začetek je danes ustvarjanje novega pogona (glejte reset).
foreach ($requests as $req) {
$engine = new Engine();
$engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
$engine->execFile($req->scriptPath);
}