API
KäivitusAPI koosneb ühest klassist: Phasis\Engine. Kõik muu (JsValue, JsFunction, JsObject, …) on mootori sisemine ning avatud ainult hostifunktsioonidele, mis ühendavad PHP↔JS. Ainus muu avalik liides on eraldiseisev parser API (klassid Phasis\Parser\Parser ja Phasis\Ast), mis parsib lähtekoodi seda käivitamata.
Loomine
use Phasis\Engine;
$engine = new Engine(); // lazy mode (default)
$engine = new Engine(eager: true); // install every built-in up front
Uus Engine pakub kogu standardteegi — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error ja keele põhisisseehitatud funktsioonid lisatakse kohe, et spetsifikatsioon toimiks (klasse ei saa olla ilma Function.prototype-ta, async/await-i ilma Promise-ta ega regulaaravaldiste literaale ilma RegExp-ta). Kõik muu — Math, JSON, Map, Set, Date, TypedArray ja massiivipuhvrite perekond, Atomics, Proxy, Reflect, console, Intl, nõrkade viidete perekond, Temporal, ShadowRealm, Web Platform Pack ja Fetch Pack — registreeritakse kohatäitja juurdepääsufunktsioonina ning luuakse esimesel lugemisel. Globaalse väärtuse lugemine on läbipaistev: Map, typeof Map, 'Map' in globalThis ja Object.keys(globalThis).includes('Map') toimivad kõik ning pärast esimest lugemist asendatakse kohatäitja tavalise andmekirjeldajaga.
Anna eager: true, kui vajad konkreetselt laiska laadimiseta käitumist — kõik sisseehitatud funktsioonid installitakse loomisel ning kogu kirjeldaja liides on introspektsiooniks nähtav. Vastavustestide käitaja (bin/test262, bin/wpt) loob mootorid innukas režiimis, et test262/WPT näeks oodatud kirjeldajate struktuure.
eval
public function eval(string $source): mixed
Parsib ja käivitab $source klassikalise skriptina mootori globaalses ulatuses ning käitab seejärel sündmussilmust, kuni see tühjeneb. Tagastab viimase hinnatud avaldise väärtuse, teisendatuna PHP-ks (või null-ks, kui programmi lõpus avaldist pole). Reegleid vaata jaotisest Väärtuste teisendamine.
$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'i ei avata: eval('Promise.resolve(5)') tagastab tühja massiivi. Kuna sündmussilmus on tühjaks töötanud selleks ajaks, kui eval() tagastab, salvesta lahendatud väärtus globaalsesse muutujasse ja loe see teise eval() abil:
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out'); // 9
Kui selgesõnalist JS throw-i ei püüta kinni, ilmub see Phasis\Exceptions\JsThrowable-na, mille sõnum on "Name: message" ja mille omadus $jsValue sisaldab visatud väärtust JsValue. Mootori enda tekitatud vead, näiteks omaduse lugemine väärtuselt null või määratlemata muutuja kasutamine, ilmuvad selle asemel kui Phasis\Exceptions\TypeError, ReferenceError või RangeError. Kõik need laiendavad Phasis\Exceptions\RuntimeError-i; parsinguvead laiendavad Phasis\Exceptions\SyntaxError-i. Vt Erandid.
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
Hindab $source-i ES-moodulina: saadaval on import / export, tipptaseme await ja import.meta ning suhtelised määrajad lahendatakse asukoha $path järgi (absoluutne failitee). Käitab sündmussilmust nagu eval() ja tagastab alati null; loe tulemusi globaalsete muutujate või konsoolipuhvri kaudu.
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
Laadija lahendab ainult suhtelisi ja absoluutseid failiteid; vt Teadaolevad piirangud.
execFile
public function execFile(string $path): mixed
Loeb faili kettalt ja edastab selle sisu funktsioonile eval(), nii et see käivitatakse klassikalise skriptina laiendist olenemata; fail import / export-ga põhjustab SyntaxErrori. Faili tegelikust teest saab dünaamiliste import() määrajate alusteekond. Moodulifailide jaoks kasuta evalAsModule().
$engine->execFile(__DIR__ . '/script.js');
Kui faili ei saa lugeda, väljastab PHP hoiatuse ja execFile() viskab lihtsa \RuntimeException-i ("Cannot read file: …").
setGlobal
public function setGlobal(string $name, mixed $value): void
Seob globaalse muutuja, mis on nähtav kõigi järgnevate JS-i hindamiste jaoks. PHP väärtused teisendatakse tüübiks JS:
| PHP tüüp | JS tüüp |
|---|---|
null |
null |
bool |
boolean |
int / float |
number |
string |
string (isegi kui see tähistab funktsiooni, näiteks 'strlen') |
| massiiv (loend) | Array (koopia) |
| massiiv (assotsiatiivne) | Object (koopia) |
Closure, käivitatav objekt, [$object, 'method'] |
function |
| mis tahes muu objekt, generaator või ressurss | undefined |
Massiivid kopeeritakse: muudatused, mida JS neis teeb, ei kajastu PHP massiivis. Lihtsaid PHP objekte ei saa veel edastada (issue #18); lahendusi vaata jaotisest Jagatud objektid.
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
Funktsiooni JS sees:
console.log(config.version); // "1.0"
log('hello'); // calls the PHP closure
call
public function call(string $name, mixed ...$args): mixed
Käivitab globaalset JS funktsiooni argumentidega PHP (teisendatakse nagu setGlobal puhul). Tagastusväärtus teisendatakse tagasi tüübiks PHP. Määratlemata nime käivitamine viskab Phasis\Exceptions\ReferenceError-i; globaalse väärtuse käivitamine, kui see pole funktsioon, viskab Phasis\Exceptions\TypeError-i.
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3); // 5
Konsooliväljund
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
console.log, console.info, console.warn, console.error ja console.debug ei väljasta teksti. Iga väljakutse lisab mootori puhvrisse ühe vormindatud rea. getConsoleLines() tagastab puhverdatud read stringide loendina, getConsoleOutput() tagastab need \n-ga ühendatuna ja clearConsole() tühjendab puhvri. Puhvrit ei tühjendata eval() väljakutsete vahel.
$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
Määrab käitusajal muudetava ressursipiirangu. Neid on täpselt üks:
| Piirang | Vaikimisi | Kirjeldus |
|---|---|---|
maxLoopIterations |
100000 |
maksimaalne iteratsioonide arv puupõhise interpretaatori käivitatava for / while / do…while tsüklikäivituse kohta |
$engine->setLimit('maxLoopIterations', 1_000_000);
Kõiki muid nimesid ignoreeritakse vaikselt; stringi pikkusele, konsooliväljundile ega seinakellaajale piiranguid pole.
Tsüklipiirangut ei rakendata veel kõikjal. Baidikoodi VM-is käitatavaid tsükleid, mis hõlmab funktsioonide kehasid ja tipptaseme programme, mida baidikoodi kompilaator aktsepteerib, ei loendata kunagi. Seega töötab while (true) {} kuni PHP selle peatab (issue #19). Ära looda, et see peatab ebausaldusväärse koodi; kasuta PHP-i max_execution_time-i või eraldi protsessi.
Piirangu saavutamisel saab vea kinni püüda JS try / catch abil (see on Error, mille sõnum on "Maximum loop iterations exceeded"). Kui seda kinni ei püüta, jõuab see PHP-ni kui Phasis\Exceptions\InternalError, mis laiendab Phasis\Exceptions\RuntimeError-i, mitte JsThrowable-i.
Mootorisse on sisse ehitatud veel kaks ülempiiri, mida ei saa käitusajal muuta: Phasis\Runtime\CallStack::maxDepth (4,096 JS kaadrit) ja JsToPhp transleeritud sulundi sügavuse piirang (8,192 PHP kaadrit). Mõlema ületamisel visatakse "Maximum call stack size exceeded", mis jõuab samuti PHP-ni kui InternalError. Need piirid ületavad tublisti mis tahes õiguspärase JS programmi vajadused — nende suurendamine nõuab koodimuudatust, mitte seadistust.
reset
public function reset(): void
Tühjendab kõik kasutaja määratud globaalsed muutujad ja konsoolipuhvri ning loob standardteegi ja sündmussilmuse uuesti.
Vaikimisi laisa laadimisega režiimis loodud mootoris kaotab reset() praegu kõik laisalt installitud sisseehitatud funktsioonid: seejärel on Map, JSON, Math, console, fetch ja ülejäänud määratlemata (issue #22). Kuni see on parandatud, loo selle asemel uus Engine või loo mootor parameetriga eager: true, mille korral reset() töötab.
Sündmussilmus
Mootor sisaldab JavaScript sündmussilmust, millel on globaalsel objektil setTimeout, setInterval, clearTimeout, clearInterval ja queueMicrotask. Mikroülesanded (Promise'i käsitlejad, queueMicrotask) viiakse lõpuni iga makroülesande vahel; taimerid käivituvad tähtaja järjekorras, iga käituskonteksti ülesannete järjekorra kaudu.
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
eval() ja evalAsModule() kutsuvad pärast sünkroonse koodi lõppu automaatselt välja runEventLoop(). Seega käivitab skript, mis ajastab tipptasemel setTimeout(cb, 5000), selle cb enne, kui eval() tagastab — silmus kasutab virtuaalaega ja nihutab kella järgmise ootel tähtajani, selle asemel et PHP magada. Vastab node script.js semantikale.
Reaalajas manustamiseks (pikaajalised serverid, UI integratsioon, päris kellal põhinev ajastamine) käivitab tickEventLoop() ühe iteratsiooni (viib mikroülesanded lõpuni ja käivitab kõik tähtaja saavutanud taimerid) ning tagastab. pendingTaskCount() annab teada ootel taimeritest; manustaja saab käitada oma tsüklit:
while ($engine->pendingTaskCount() > 0) {
$engine->tickEventLoop();
usleep(1000);
}
Lõputu setInterval on piiratud: runEventLoop() seab ülempiiriks 100,000 tsüklit ja viskab lihtsa \RuntimeException-i — vea põhjus on selgelt JS koodis, mis põhjustab arusaadava PHP vea, mitte hangumise. Intervall jääb ajastatuks, mistõttu iga hilisem eval() samas mootoris viskab sama vea; seejärel kõrvalda mootor kasutusest. Taimeri tagasihelistuses kinni püüdmata vead väljastatakse funktsiooniga error_log() ja silmus jätkab tööd; üks vigane taimer ei peata programmi.
Fetchi haagid
Phasis tarnitakse vaikimisi lubatud fetch()-ga, mille aluseks on PHP ext-curl. Kolm haaki võimaldavad manustajal vahetada transpordikihti, piirata päringuid või lubada küpsiste salvestamise. Kõik kolm aktsepteerivad kas PHP väljakutsutavat väärtust või objekti meetodit; väärtuse kuju lepingut vaata jaotisest Veebi API-d.
setFetchTransport
public function setFetchTransport(callable $transport): void
Asendab vaikimisi kasutatava CurlTransporti. Väljakutsutav väärtus saab päringu kirjeldaja ja valikulise AbortSignal JsObjecti ning tagastab vastuse kirjeldaja.
// 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(),
];
});
Levinud manustamisviis: testides paigaldatakse näidistransport, tootmiskeskkonnas päris curl-transport.
setFetchPolicy
public function setFetchPolicy(callable $hook): void
Päringueelne poliitikahaak. Saab Request JsObjecti; võib keelamiseks vea visata, ümberkirjutamiseks muudetud Request tagastada või lubamiseks tagastada null/mitte midagi.
// 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
Valikuline küpsiste salvesti. Aktsepteerib mis tahes objekti, millel on get(string $url): string ja set(string $url, string $header): void — kas PHP- või JS-poolel.
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());
Ilma setCookieJar()-ta ignoreerib fetch nii sissetulevaid Set-Cookie- kui ka väljaminevaid Cookie-päiseid — vaikimisi küpsiseid ei kasutata.
Erandid
| Klass | Laiendab | Millal |
|---|---|---|
Phasis\Exceptions\JsThrowable |
RuntimeError |
selgesõnaline JS throw, mida kinni ei püütud |
Phasis\Exceptions\TypeError, ReferenceError, RangeError |
RuntimeError |
mootori enda tekitatud viga (näiteks null.x või määratlemata muutuja), mida kinni ei püütud |
Phasis\Exceptions\InternalError |
RuntimeError |
ületati tsüklite piirang või väljakutsete pinu ülempiir |
Phasis\Exceptions\RuntimeError |
\RuntimeException |
ülaltoodute põhiklass |
Phasis\Parser\ParseError |
Phasis\Exceptions\SyntaxError (\RuntimeException) |
lähtekoodi ei saa parsida |
\RuntimeException |
execFile() ei saa faili lugeda; sündmussilmus jõudis 100,000 tsükli piirini |
Skripti kõigi tõrgete kinnipüüdmiseks püüa kinni Phasis\Exceptions\RuntimeError ja Phasis\Exceptions\SyntaxError või lihtsalt \RuntimeException.
Teie enda PHP hostifunktsioonide visatud erandid on teistsugused: praegu läbivad need JS try / catch muutmata kujul ning jõuavad funktsiooni eval() väljakutsujani oma algse klassiga (issue #20). Vt Hostifunktsioonid.
Elutsükkel
Engine loomine on kiire (~paar ms külmkäivituse korral) ja standardteek laaditakse laisalt. Enamiku manustamisjuhtude puhul piisab ühest pikaajalisest mootorist protsessi kohta. Mitme rentnikuga kasutamisel anna igale rentnikule või päringule oma mootor, et globaalsed muutujad nende vahel ei lekiks. Praegu on usaldusväärseim viis puhta algseisu saamiseks luua uus mootor (vt reset).
foreach ($requests as $req) {
$engine = new Engine();
$engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
$engine->execFile($req->scriptPath);
}