API
Izpildes API ir viena klase: Phasis\Engine. Viss pārējais (JsValue, JsFunction, JsObject, …) ir dzinēja iekšējā daļa un pieejams tikai resursdatora funkcijām, kas sasaista PHP↔JS. Vienīgā cita publiskā saskarne ir patstāvīgais parsētājsAPI (Phasis\Parser\Parser un Phasis\Ast klases), kas parsē avota kodu, to neizpildot.
Izveide
use Phasis\Engine;
$engine = new Engine(); // lazy mode (default)
$engine = new Engine(eager: true); // install every built-in up front
Jauns Engine nodrošina pilnu standarta bibliotēku — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error un valodas pamatfunkcijas tiek uzstādītas uzreiz, lai darbotos specifikācija (bez Function.prototype nevar izmantot klases, bez Promise — async/await, bez RegExp — regex literāļus). Viss pārējais — Math, JSON, Map, Set, Date, TypedArray un masīva buferu saime, Atomics, Proxy, Reflect, console, Intl, vājo atsauču saime, Temporal, ShadowRealm, Web Platform Pack un Fetch Pack — tiek reģistrēts kā viettura piekļuves metode un materializēts pirmajā nolasīšanas reizē. Globālā mainīgā nolasīšana ir caurspīdīga: Map, typeof Map, 'Map' in globalThis, Object.keys(globalThis).includes('Map') darbojas, un pēc pirmās nolasīšanas vietturu aizstāj parastu datu deskriptoru.
Norādiet eager: true, ja īpaši nepieciešama iepriekšējā uzvedība bez slinkās ielādes — visas iebūvētās funkcijas tiek instalētas izveides laikā, un visa deskriptoru saskarne ir pieejama introspekcijai. Atbilstības testu ietvars (bin/test262, bin/wpt) izveido dzinējus tūlītējās ielādes režīmā, lai test262/WPT redzētu gaidītās deskriptoru formas.
eval
public function eval(string $source): mixed
Parsē un izpilda $source kā klasisku skriptu dzinēja globālajā tvērumā, pēc tam darbina notikumu ciklu, līdz tas ir tukšs. Atgriež pēdējās novērtētās izteiksmes vērtību, pārveidotu par PHP (vai null, ja programma beidzās bez izteiksmes). Noteikumus skatiet sadaļā Vērtību pārveidošana.
$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 netiek atrisināts: eval('Promise.resolve(5)') atgriež tukšu masīvu. Tā kā eval() atgriešanās brīdī notikumu cikls jau ir iztukšots, saglabājiet atrisināto vērtību globālajā mainīgajā un nolasiet to ar otru eval():
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out'); // 9
Ja skaidri izsauktais JS throw netiek pārtverts, tas tiek parādīts kā Phasis\Exceptions\JsThrowable, kura ziņojums ir "Name: message" un kuras rekvizīts $jsValue satur izmesto JsValue. Dzinēja pašas radītās kļūdas, piemēram, īpašības nolasīšana no null vai nedefinēts mainīgais, tiek parādītas kā Phasis\Exceptions\TypeError, ReferenceError vai RangeError. Tās visas ir Phasis\Exceptions\RuntimeError atvasinātas klases; parsēšanas kļūdas ir Phasis\Exceptions\SyntaxError atvasinātas. Skatiet sadaļu Izņēmumi.
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
Novērtē $source kā ES moduli: ir pieejami import / export, augšējā līmeņa await un import.meta, un relatīvie specifikatori tiek atrisināti attiecībā pret $path (absolūtu faila ceļu). Notikumu cikls darbojas kā eval(), un vienmēr tiek atgriezts null; rezultātus nolasiet, izmantojot globālos mainīgos vai konsoles buferi.
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
Ielādētājs atrisina tikai relatīvos un absolūtos ceļus; skatiet Zināmie ierobežojumi.
execFile
public function execFile(string $path): mixed
Nolasa failu no diska un nodod tā saturu uz eval(), tāpēc tas tiek izpildīts kā klasisks skripts neatkarīgi no faila paplašinājuma; fails ar import / export izraisa SyntaxError. Faila īstais ceļš kļūst par dinamisko import() specifikatoru pamatu. Moduļu failiem izmantojiet evalAsModule().
$engine->execFile(__DIR__ . '/script.js');
Ja failu nevar nolasīt, PHP parāda brīdinājumu, bet execFile() izmet vienkāršu \RuntimeException ("Cannot read file: …").
setGlobal
public function setGlobal(string $name, mixed $value): void
Saista globālu mainīgo, kas ir redzams visiem turpmākajiem JS novērtējumiem. PHP vērtības tiek pārveidotas par JS:
| PHP tips | JS tips |
|---|---|
null |
null |
bool |
boolean |
int / float |
number |
string |
string (arī tad, ja tas apzīmē funkciju, piemēram, 'strlen') |
| masīvs (saraksts) | Array (kopija) |
| masīvs (asociatīvs) | Object (kopija) |
Closure, izsaucams objekts, [$object, 'method'] |
function |
| jebkurš cits objekts, ģenerators vai resurss | undefined |
Masīvi tiek kopēti: izmaiņas, ko tajos veic JS, nav redzamas PHP masīvā. Vienkāršus PHP objektus pagaidām nevar nodot (18. problēma); risinājumus skatiet sadaļā Koplietojamie objekti.
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
Iekš JS:
console.log(config.version); // "1.0"
log('hello'); // calls the PHP closure
call
public function call(string $name, mixed ...$args): mixed
Izsauc globālu JS funkciju ar PHP argumentiem (pārveidotiem tāpat kā setGlobal gadījumā). Atgrieztā vērtība tiek pārveidota atpakaļ par PHP. Izsaucot nedefinētu nosaukumu, tiek izraisīts Phasis\Exceptions\ReferenceError; izsaucot globālo mainīgo, kas nav funkcija, tiek izraisīts Phasis\Exceptions\TypeError.
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3); // 5
Konsoles izvade
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
console.log, console.info, console.warn, console.error un console.debug neko neizvada. Katrs izsaukums pievieno vienu formatētu rindu dzinēja buferim. getConsoleLines() atgriež buferī esošās rindas kā virkņu sarakstu, getConsoleOutput() atgriež tās, savienotas ar \n, bet clearConsole() iztukšo buferi. Starp eval() izsaukumiem buferis netiek notīrīts.
$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
Iestata izpildlaikā maināmu resursa ierobežojumu. Tāds ir tieši viens:
| Ierobežojums | Noklusējums | Apraksts |
|---|---|---|
maxLoopIterations |
100000 |
maksimālais iterāciju skaits katrā koka pastaigas interpretētāja izpildītajā for / while / do…while ciklā |
$engine->setLimit('maxLoopIterations', 1_000_000);
Jebkurš cits nosaukums tiek klusi ignorēts; nav ierobežojumu virkņu garumam, konsoles izvadei vai izpildes laikam.
Cikla ierobežojums vēl netiek piemērots visur. Bytecode VM izpildītie cikli, kas aptver funkciju pamatdaļas un augstākā līmeņa programmas, kuras pieņem bytecode kompilators, netiek skaitīti, tāpēc while (true) {} turpina darboties, līdz to aptur PHP (19. problēma). Neuzticieties tam, lai apturētu neuzticamu kodu; izmantojiet PHP max_execution_time vai atsevišķu procesu.
Kad ierobežojums sasniegts, kļūdu var uztvert ar JS try / catch (kā Error ar ziņojumu "Maximum loop iterations exceeded"). Ja nekas to neuztver, tā sasniedz PHP kā Phasis\Exceptions\InternalError, kas paplašina Phasis\Exceptions\RuntimeError, nevis JsThrowable.
Dzinējā ir iebūvēti vēl divi griesti, kurus nevar mainīt izpildlaikā: Phasis\Runtime\CallStack::maxDepth (4,096 JS kadri) un JsToPhp transpēlēto aizvēruma dziļuma sargs (8,192 PHP kadri). Pārsniedzot jebkuru no tiem, tiek izraisīts "Maximum call stack size exceeded", kas arī sasniedz PHP kā InternalError. Tie ir krietni augstāki par jebkuras leģitīmas JS programmas vajadzībām; to palielināšanai jāmaina kods, konfigurācijas iestatījuma tam nav.
reset
public function reset(): void
Notīra visus lietotāja definētos globālos mainīgos un konsoles buferi, kā arī no jauna izveido standarta bibliotēku un notikumu ciklu.
Dzinējā, kas izveidots noklusējuma slinkajā režīmā, reset() pašlaik zaudē visus slinki instalētos iebūvētos objektus: pēc tam Map, JSON, Math, console, fetch un pārējie nav definēti (22. problēma). Kamēr tas nav novērsts, izveidojiet jaunu Engine vai izveidojiet to ar eager: true, kur darbojas reset().
Notikumu cikls
Dzinējam ir JavaScript notikumu cikls ar setTimeout, setInterval, clearTimeout, clearInterval un queueMicrotask globālajā objektā. Mikrouzdevumi (Promise apstrādātāji, queueMicrotask) tiek izpildīti starp katru makrouzdevumu; taimeri nostrādā termiņa secībā, izmantojot katras vides uzdevumu rindu.
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
eval() un evalAsModule() automātiski izsauc runEventLoop() pēc sinhronā koda izpildes. Tādēļ skriptā, kas augšējā līmenī ieplāno setTimeout(cb, 5000), tas tiek izpildīts cb, pirms eval() atgriežas — cikls izmanto virtuālo laiku, pārvietojot pulksteni uz nākamo gaidāmo termiņu, nevis gaidot PHP. Atbilst node script.js semantikai.
Reāllaika iegulšanai (ilgstoši darbojošos serveros, UI integrācijā, plānošanā pēc reāla pulksteņa) tickEventLoop() izpilda vienu iterāciju (izpilda mikrouzdevumus, iedarbina visus pienākušos taimerus) un atgriežas. pendingTaskCount() ziņo par gaidošajiem taimeriem; iegultā lietotne var pati vadīt izpildes ciklu:
while ($engine->pendingTaskCount() > 0) {
$engine->tickEventLoop();
usleep(1000);
}
Nekontrolēta setInterval darbība ir ierobežota: runEventLoop() apstājas pēc 100,000 cikliem un izmet vienkāršu \RuntimeException — skaidra PHP kļūda parāda JS koda vainu, nevis ļauj procesam uzkārties. Intervāls paliek ieplānots, tāpēc katrs nākamais eval() tajā pašā dzinējā izmet to pašu kļūdu; pēc tam dzinēju izmetiet. Neuztvertās kļūdas taimera atzvanā tiek izvadītas, izmantojot error_log(), un cikls turpina izpildi; viens kļūdains taimeris neaptur programmu.
Fetch āķi
Phasis pēc noklusējuma ir ieslēgts fetch(), kurā par transportu tiek izmantots PHP ext-curl. Trīs āķi ļauj iegultajai lietotnei nomainīt transportu, ierobežot pieprasījumus vai ieslēgt sīkfailu glabāšanu. Katrs no tiem pieņem vai nu PHP izsaucamu funkciju, vai metodi objektā; pieļaujamo vērtību struktūru skatiet sadaļā Tīmekļa API.
setFetchTransport
public function setFetchTransport(callable $transport): void
Aizstāj noklusējuma CurlTransport. Izsaucamā funkcija saņem pieprasījuma deskriptoru un neobligātu AbortSignal JsObject; atgriež atbildes deskriptoru.
// 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(),
];
});
Izplatīts iegulšanas paņēmiens: testos instalēt imitētu transportu, bet produkcijā — īsto curl transportu.
setFetchPolicy
public function setFetchPolicy(callable $hook): void
Iepriekšējas pārbaudes politikas āķis. Saņem Request JsObject; var izsaukt kļūdu, lai noraidītu, atgriezt modificētu Request, lai pārrakstītu, vai atgriezt null / neatgriezt vērtību, lai atļautu.
// 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
Neobligāta sīkfailu krātuve. Pieņem jebkuru objektu, kas nodrošina get(string $url): string un set(string $url, string $header): void — PHP vai JS pusē.
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());
Ja nav setCookieJar(), fetch ignorē gan ienākošās Set-Cookie, gan izejošās Cookie galvenes — pēc noklusējuma sīkfaili netiek izmantoti.
Izņēmumi
| Klase | Paplašina | Kad |
|---|---|---|
Phasis\Exceptions\JsThrowable |
RuntimeError |
nepārprotami JS throw, ko nekas neuztvēra |
Phasis\Exceptions\TypeError, ReferenceError, RangeError |
RuntimeError |
dzinēja paša izraisīta kļūda (piemēram, null.x vai nedefinēts mainīgais), ko nekas neuztvēra |
Phasis\Exceptions\InternalError |
RuntimeError |
pārsniegts cikla ierobežojums vai izsaukumu steka limits |
Phasis\Exceptions\RuntimeError |
\RuntimeException |
iepriekš minēto klašu bāzes klase |
Phasis\Parser\ParseError |
Phasis\Exceptions\SyntaxError (\RuntimeException) |
avotu nevar parsēt |
\RuntimeException |
execFile() nevar nolasīt failu; notikumu cikls sasniedzis 100,000 ciklu ierobežojumu |
Lai uztvertu visas skripta kļūmes, uztveriet Phasis\Exceptions\RuntimeError un Phasis\Exceptions\SyntaxError vai vienkārši \RuntimeException.
Paša PHP resursdatora funkciju izraisītie izņēmumi atšķiras: pašlaik tie nemainīti iziet cauri JS try / catch un sasniedz eval() izsaucēju, saglabājot sākotnējo klasi (20. problēma). Skatiet sadaļu Resursdatora funkcijas.
Dzīves cikls
Engine izveide ir ātra (aukstā palaišanā ~dažas ms), un standarta bibliotēka tiek instalēta pēc vajadzības. Vairumā iegulšanas gadījumu procesam pietiek ar vienu ilgstoši darbojošos dzinēju. Vairāku klientu lietošanai piešķiriet katram klientam vai pieprasījumam savu dzinēju, lai globālie mainīgie starp tiem nenoplūstu. Pašlaik uzticamākais veids, kā sākt ar tīru stāvokli, ir izveidot jaunu dzinēju (skatiet atiestatīšana).
foreach ($requests as $req) {
$engine = new Engine();
$engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
$engine->execFile($req->scriptPath);
}