Preskoči na vsebino

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

PHP
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

PHP
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.

PHP
$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():

PHP
$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.

PHP
try {
    $engine->eval('throw new Error("boom")');
} catch (\Phasis\Exceptions\JsThrowable $e) {
    echo $e->getMessage();
    // Error: boom
}

evalAsModule

PHP
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.

PHP
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);

Nalagalnik razreši samo relativne in absolutne poti; glejte Znane omejitve.

execFile

PHP
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().

PHP
$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

PHP
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.

PHP
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));

Znotraj JS:

JavaScript
console.log(config.version);   // "1.0"
log('hello');                  // calls the PHP closure

call

PHP
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.

PHP
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);   // 5

Izpis konzole

PHP
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.

PHP
$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

PHP
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
PHP
$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

PHP
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.

PHP
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:

PHP
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

PHP
public function setFetchTransport(callable $transport): void

Zamenja privzeti CurlTransport. Klicni objekt prejme opis zahtevka in izbirni AbortSignal JsObject; vrne opis odgovora.

PHP
// 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

PHP
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.

PHP
// 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

PHP
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.

PHP
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).

PHP
foreach ($requests as $req) {
    $engine = new Engine();
    $engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
    $engine->execFile($req->scriptPath);
}