Pereiti prie turinio

API

Vykdymo API sudaro viena klasė: Phasis\Engine. Visa kita (JsValue, JsFunction, JsObject, …) yra vidinė variklio dalis ir pasiekiama tik prieglobos funkcijoms, kurios sujungia PHP↔JS. Kita viešoji sąsaja — atskiras analizatorius API (Phasis\Parser\Parser ir Phasis\Ast klasės), analizuojantis šaltinio kodą jo nevykdydamas.

Sukūrimas

PHP
use Phasis\Engine;

$engine = new Engine();                 // lazy mode (default)
$engine = new Engine(eager: true);      // install every built-in up front

Naujas Engine suteikia visą standartinę biblioteką — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error ir pagrindines kalbos integruotas funkcijas; jos įdiegiamos iš karto, kad specifikacija veiktų (klasės neveiks be Function.prototype, async/await — be Promise, regex literalai — be RegExp). Visa kita — Math, JSON, Map, Set, Date, TypedArray ir masyvų buferių šeima, Atomics, Proxy, Reflect, console, Intl, silpnųjų nuorodų šeima, Temporal, ShadowRealm, Web Platform Pack ir Fetch Pack — užregistruojama kaip pakaitinis prieigos metodas ir sukuriama pirmą kartą nuskaičius. Globaliojo objekto reikšmę galima skaityti įprastai: veikia Map, typeof Map, 'Map' in globalThis ir Object.keys(globalThis).includes('Map'), o po pirmojo skaitymo pakaitinis metodas pakeičiamas įprastu duomenų deskriptoriumi.

Perduokite eager: true, jei reikia elgsenos iki atidėtojo įdiegimo — visos integruotos funkcijos įdiegiamos kuriant objektą, o introspekcija mato visą deskriptoriaus sąsają. Atitikties testų karkasas (bin/test262, bin/wpt) kuria variklius išankstinio įdiegimo režimu, kad test262/WPT matytų numatytų formų deskriptorius.

eval

PHP
public function eval(string $source): mixed

Analizuoja ir vykdo $source kaip klasikinį scenarijų variklio visuotinėje srityje, tada vykdo įvykių ciklą, kol jis ištuštėja. Grąžina paskutinės įvertintos išraiškos reikšmę, konvertuotą į PHP (arba null, jei programos pabaigoje nebuvo išraiškos). Taisyklės aprašytos skiltyje Reikšmių konvertavimas.

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 reikšmė neišskleidžiama: eval('Promise.resolve(5)') grąžina tuščią masyvą. Kadangi grįžus iš eval() įvykių ciklas jau būna ištuštėjęs, išsaugokite įvykdytą reikšmę globaliajame kintamajame ir perskaitykite ją antru eval() iškvietimu:

PHP
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out');   // 9

Aiškiai išmesta JS throw, kurios niekas neperima, pateikiama kaip Phasis\Exceptions\JsThrowable; jos pranešimas yra "Name: message", o savybėje $jsValue saugoma išmesta reikšmė JsValue. Variklio pačios sukeltos klaidos, pavyzdžiui, savybės skaitymas iš null arba neapibrėžtas kintamasis, pateikiamos kaip Phasis\Exceptions\TypeError, ReferenceError arba RangeError. Visos jos paveldi Phasis\Exceptions\RuntimeError; analizavimo klaidos paveldi Phasis\Exceptions\SyntaxError. Žr. Išimtys.

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

Įvertina $source kaip ES modulį: pasiekiami import / export, aukščiausio lygio await ir import.meta, o santykiniai specifikatoriai sprendžiami pagal $path (absoliutų failo kelią). Įvykių ciklas vykdomas kaip naudojant eval(), o rezultatas visada yra null; rezultatus skaitykite per globaliuosius kintamuosius arba konsolės buferį.

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

Įkėliklis išsprendžia tik santykinius ir absoliučius kelius; žr. Žinomi apribojimai.

execFile

PHP
public function execFile(string $path): mixed

Nuskaito failą iš disko ir perduoda jo turinį į eval(), todėl šis vykdomas kaip klasikinis scenarijus, nepriklausomai nuo failo plėtinio; failas su import / export sukelia SyntaxError. Tikrasis failo kelias tampa dinaminių import() specifikatorių baziniu keliu. Modulių failams naudokite evalAsModule().

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

Jei failo nepavyksta nuskaityti, PHP pateikia įspėjimą, o execFile() išmeta paprastą \RuntimeException („Cannot read file: …“).

setGlobal

PHP
public function setGlobal(string $name, mixed $value): void

Susieja globalųjį kintamąjį, matomą visuose vėlesniuose JS vertinimuose. PHP reikšmės konvertuojamos į JS:

PHP tipas JS tipas
null null
bool boolean
int / float number
string string (net jei tai funkcijos pavadinimas, pvz., 'strlen')
masyvas (sąrašas) Array (kopija)
masyvas (asociatyvusis) Object (kopija)
Closure, iškviečiamas objektas, [$object, 'method'] function
bet kuris kitas objektas, generatorius arba išteklius undefined

Masyvai kopijuojami: JS atlikti pakeitimai juose neatsispindi PHP masyve. Paprastųjų PHP objektų dar negalima perduoti (18 problema); apie galimus sprendimus žr. Bendrinami objektai.

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

JS viduje:

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

call

PHP
public function call(string $name, mixed ...$args): mixed

Iškviečia globaliąją JS funkciją su PHP argumentais (konvertuojamais taip pat, kaip naudojant setGlobal). Grąžinama reikšmė konvertuojama atgal į PHP. Iškvietus neapibrėžtą vardą išmetama Phasis\Exceptions\ReferenceError; iškvietus globalųjį objektą, kuris nėra funkcija, išmetama Phasis\Exceptions\TypeError.

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

Konsolės išvestis

PHP
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void

console.log, console.info, console.warn, console.error ir console.debug nieko nespausdina. Kiekvienas iškvietimas prideda vieną suformatuotą eilutę į variklio buferį. getConsoleLines() grąžina buferio eilutes kaip eilučių sąrašą, getConsoleOutput() sujungia jas į vieną eilutę naudodama \n, o clearConsole() išvalo buferį. Buferis neišvalomas tarp eval() iškvietimų.

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

Nustato vykdymo metu keičiamą išteklių ribą. Yra tik viena:

Riba Numatytoji reikšmė Aprašymas
maxLoopIterations 100000 didžiausias iteracijų skaičius per for / while / do…while medžio interpretatoriaus vykdomą ciklą
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Kiti pavadinimai tyliai ignoruojami; nėra eilučių ilgio, konsolės išvesties ar realaus laiko apribojimų.

Ciklo riba dar ne visur taikoma. Baitkodo VM vykdomi ciklai, apimantys funkcijų turinius ir aukščiausio lygio programas, kurias priima baitkodo kompiliatorius, neskaičiuojami, todėl while (true) {} veikia, kol jo nesustabdo PHP (19 problema). Nepasitikėkite šia riba vykdydami nepatikimą kodą; naudokite PHP max_execution_time arba atskirą procesą.

Pasiekus ribą, klaidą galima perimti naudojant JS try / catch (kaip Error su pranešimu „Maximum loop iterations exceeded“). Jei niekas jos neperima, ji pasiekia PHP kaip Phasis\Exceptions\InternalError, kuri paveldi Phasis\Exceptions\RuntimeError, o ne JsThrowable.

Dar dvi variklyje užkoduotos ir vykdymo metu nekeičiamų ribų: Phasis\Runtime\CallStack::maxDepth (4,096 JS kadrai) ir JsToPhp transpiliuojamų uždarųjų funkcijų gylio apsauga (8,192 PHP kadrai). Viršijus bet kurią, išmetama „Maximum call stack size exceeded“; ji taip pat pasiekia PHP kaip InternalError. Šios ribos gerokai viršija bet kurios teisėtos JS programos poreikius — jas padidinti galima keičiant kodą, o ne konfigūracijos parinktį.

reset

PHP
public function reset(): void

Išvalo visus naudotojo apibrėžtus globaliuosius kintamuosius ir konsolės buferį, iš naujo sukuria standartinę biblioteką bei įvykių ciklą.

Variklyje, sukurtame numatytuoju atidėtojo įdiegimo režimu, reset() šiuo metu pašalina visas atidėtai įdiegtas integruotas funkcijas: vėliau Map, JSON, Math, console, fetch ir kitos tampa neapibrėžtos (22 problema). Kol tai bus ištaisyta, vietoj to sukurkite naują Engine arba kurdami perduokite eager: true — tada veikia reset().

Įvykių ciklas

Variklyje yra JavaScript įvykių ciklas; globaliajame objekte pasiekiami setTimeout, setInterval, clearTimeout, clearInterval ir queueMicrotask. Mikroužduotys (Promise tvarkyklės, queueMicrotask) ištuštinamos tarp kiekvienos makroužduoties; laikmačiai suveikia nustatyta termino tvarka per kiekvienos srities užduočių eilę.

PHP
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop

eval() ir evalAsModule() automatiškai iškviečia runEventLoop() pasibaigus sinchroniniam kodui. Todėl scenarijus, kuris aukščiausiame lygyje nustato setTimeout(cb, 5000), įvykdo cb prieš grįžtant eval() — ciklas naudoja virtualųjį laiką ir perstumia laikrodį iki kito laukiančio termino, užuot laukęs PHP. Atitinka node script.js semantiką.

Jei norite įterpti į realiuoju laiku veikiančią sistemą (ilgalaikius serverius, UI integraciją ar planavimą pagal tikrą laikrodį), tickEventLoop() įvykdo vieną iteraciją (ištuština mikroužduotis, paleidžia atėjusius laikmačius) ir grįžta. pendingTaskCount() pateikia laukiančių laikmačių sąrašą; įterpianti programa gali valdyti savo ciklą:

PHP
while ($engine->pendingTaskCount() > 0) {
    $engine->tickEventLoop();
    usleep(1000);
}

Nesibaigiantis setInterval yra apribotas: runEventLoop() riboja iki 100,000 ciklų ir išmeta paprastą \RuntimeException — JS kodo klaida pateikiama kaip aiški PHP klaida, o ne pakibimas. Intervalas lieka suplanuotas, todėl kiekvienas vėlesnis eval() tame pačiame variklyje išmeta tą pačią klaidą; po to variklį pašalinkite. Neperimtos laikmačio atgalinio iškvietimo klaidos perduodamos per error_log(), o ciklas vykdomas toliau; viena sugedusi laikmačio užduotis nesustabdo programos.

Fetch kabliai

Phasis pagal numatytuosius nustatymus pateikiamas su įjungtu fetch(); pagrindinis transportas yra PHP ext-curl. Trys kabliai leidžia įterpiančiai programai pakeisti transportą, apriboti užklausas arba įjungti slapukų saugyklą. Visi priima iškviečiamą PHP arba objekto metodą; reikšmių formos aprašytos skiltyje Žiniatinklio API.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Pakeičia numatytąjį CurlTransport. Iškviečiamai funkcijai perduodamas užklausos deskriptorius ir pasirinktinis AbortSignal JsObject; ji grąžina atsakymo deskriptorių.

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

Dažnas integravimo būdas: testuose įdiegti imitacinį transportą, o produkcinėje aplinkoje — tikrą curl transportą.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Išankstinio patikrinimo politikos kablys. Gauna Request JsObject; užklausai atmesti gali išmesti klaidą, perrašyti ją grąžindamas pakeistą Request arba leisti grąžindamas null / negrąžindamas reikšmės.

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

Pasirinktinai įjungiama slapukų saugykla. Priima bet kurį objektą, turintį get(string $url): string ir set(string $url, string $header): void metodus — PHP arba JS pusėje.

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

Be setCookieJar(), fetch ignoruoja ir gaunamas Set-Cookie, ir siunčiamas Cookie antraštes — pagal numatytuosius nustatymus slapukai nenaudojami.

Išimtys

Klasė Paveldi iš Kada naudojama
Phasis\Exceptions\JsThrowable RuntimeError aiškiai išmesta JS throw, kurios niekas neperėmė
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError variklio sukelta ir niekieno neperimta klaida (pvz., null.x arba neapibrėžtas kintamasis)
Phasis\Exceptions\InternalError RuntimeError viršyta ciklo riba arba iškvietimų steko riba
Phasis\Exceptions\RuntimeError \RuntimeException visų pirmiau išvardytų klasių bazinė klasė
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) šaltinio kodo nepavyksta išanalizuoti
\RuntimeException execFile() negali nuskaityti failo; įvykių ciklas pasiekė 100,000 ciklų ribą

Jei norite perimti visas scenarijaus klaidas, perimkite Phasis\Exceptions\RuntimeError ir Phasis\Exceptions\SyntaxError arba tiesiog \RuntimeException.

Jūsų pačių PHP prieglobos funkcijų išmetamos išimtys tvarkomos kitaip: šiuo metu jos nepakitusios praeina pro JS try / catch ir pasiekia eval() iškvietusį objektą, išsaugodamos pradinę klasę (20 problema). Žr. Prieglobos funkcijos.

Gyvavimo ciklas

Engine sukuriamas greitai (šaltai per kelias milisekundes), o standartinė biblioteka įdiegiama atidėtai. Daugeliu integravimo atvejų užtenka vieno ilgai veikiančio variklio kiekviename procese. Naudodami keliems nuomininkams, kiekvienam nuomininkui arba užklausai skirkite atskirą variklį, kad globalieji kintamieji nebūtų bendrinami. Šiuo metu patikimiausias būdas pradėti nuo švarios būsenos — sukurti naują variklį (žr. atstatymą).

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