Liigu sisu juurde

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

PHP
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

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

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)

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:

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

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

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.

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

Laadija lahendab ainult suhtelisi ja absoluutseid failiteid; vt Teadaolevad piirangud.

execFile

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

PHP
$engine->execFile(__DIR__ . '/script.js');

Kui faili ei saa lugeda, väljastab PHP hoiatuse ja execFile() viskab lihtsa \RuntimeException-i ("Cannot read file: …").

setGlobal

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

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

Funktsiooni JS sees:

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

call

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

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

Konsooliväljund

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

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

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

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

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

PHP
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

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

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(),
    ];
});

Levinud manustamisviis: testides paigaldatakse näidistransport, tootmiskeskkonnas päris curl-transport.

setFetchPolicy

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

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

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.

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());

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

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