Siirry sisältöön

API

Suorituksen API on yksi luokka: Phasis\Engine. Kaikki muu (JsValue, JsFunction, JsObject, …) on moottorin sisäistä ja näkyy vain isäntäfunktioille, jotka yhdistävät PHP↔JS. Ainoa muu julkinen rajapinta on itsenäinen jäsennin API (Phasis\Parser\Parser- ja Phasis\Ast-luokat), joka jäsentää lähdekoodin suorittamatta sitä.

Luominen

PHP
use Phasis\Engine;

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

Uusi Engine tarjoaa koko standardikirjaston — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error sekä kielen keskeiset sisäiset toiminnot asennetaan heti, jotta määritys toimii (luokkia ei voi käyttää ilman kohdetta Function.prototype, async/awaitia ilman kohdetta Promise eikä regex-literaaleja ilman kohdetta RegExp). Kaikki muu — Math, JSON, Map, Set, Date, TypedArray ja array-buffer-perhe, Atomics, Proxy, Reflect, console, Intl, weak-ref-perhe, Temporal, ShadowRealm, Web Platform Pack ja Fetch Pack — rekisteröidään paikkamerkkihakijana ja luodaan ensimmäisellä lukukerralla. Globaalin lukeminen on läpinäkyvää: Map, typeof Map, 'Map' in globalThis ja Object.keys(globalThis).includes('Map') toimivat, ja ensimmäisen lukukerran jälkeen paikkamerkki korvataan tavallisella data-deskriptorilla.

Välitä eager: true, jos tarvitset nimenomaan toimintatavan ennen laiskaa alustusta — kaikki sisäänrakennetut toiminnot asennetaan luonnin yhteydessä ja koko kuvaileva pinta on nähtävissä tarkastelua varten. Vaatimustenmukaisuustestien ajoympäristö (bin/test262, bin/wpt) luo moottorit tässä välittömässä alustustilassa, jotta test262 ja WPT näkevät odottamansa deskriptorien muodot.

eval

PHP
public function eval(string $source): mixed

Jäsentää ja suorittaa kohteen $source klassisena skriptinä moottorin globaalissa käyttöalueessa ja suorittaa sitten tapahtumasilmukkaa, kunnes se on tyhjä. Palauttaa viimeksi arvioidun lausekkeen arvon muunnettuna muotoon PHP (tai null, jos ohjelman lopussa ei ollut lauseketta). Katso säännöt kohdasta Arvojen muuntaminen.

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)

Promisea ei pureta: eval('Promise.resolve(5)') palauttaa tyhjän taulukon. Koska tapahtumasilmukka on tyhjentynyt siihen mennessä, kun eval() palauttaa, tallenna ratkennut arvo globaaliin muuttujaan ja lue se toisella eval()-kutsulla:

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

Eksplisiittinen JS throw, jota mikään ei sieppaa, ilmenee muodossa Phasis\Exceptions\JsThrowable, jonka viesti on "Name: message" ja jonka $jsValue-ominaisuus sisältää heitetyn JsValue-arvon. Moottorin itse aiheuttamat virheet, kuten ominaisuuden lukeminen arvosta null tai määrittelemätön muuttuja, ilmenevät sen sijaan muodossa Phasis\Exceptions\TypeError, ReferenceError tai RangeError. Kaikki nämä perivät luokan Phasis\Exceptions\RuntimeError; jäsennysvirheet perivät luokan Phasis\Exceptions\SyntaxError. Katso Poikkeukset.

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

Suorittaa $source-arvon ES-moduulina: import / export, ylimmän tason await ja import.meta ovat käytettävissä, ja suhteelliset määrittäjät ratkaistaan suhteessa arvoon $path (absoluuttinen tiedostopolku). Suorittaa tapahtumasilmukan kuten eval() ja palauttaa aina arvon null; lue tulokset globaaleista muuttujista tai konsolipuskurista.

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

Lataaja ratkaisee vain suhteelliset ja absoluuttiset polut; katso Tunnetut rajoitukset.

execFile

PHP
public function execFile(string $path): mixed

Lukee tiedoston levyltä ja välittää sen sisällön arvolle eval(), joten se suoritetaan tavallisena skriptinä tiedostopäätteestä riippumatta; tiedosto, jossa on import / export, aiheuttaa SyntaxError-virheen. Tiedoston todellista polkua käytetään dynaamisten import()-määrittäjien perustana. Käytä moduulitiedostoille toimintoa evalAsModule().

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

Jos tiedostoa ei voi lukea, PHP antaa varoituksen ja execFile() heittää tavallisen \RuntimeException-virheen ("Tiedostoa ei voi lukea: …").

setGlobal

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

Sitoo globaalin muuttujan, joka näkyy kaikissa myöhemmissä JS-arvioinneissa. PHP-arvot muunnetaan tyypiksi JS:

PHP-tyyppi JS-tyyppi
null null
bool boolean
int / float number
string string (myös silloin, kun se nimeää funktion, kuten 'strlen')
taulukko (lista) Array (kopio)
taulukko (assosiatiivinen) Object (kopio)
Closure, kutsuttava olio, [$object, 'method'] function
mikä tahansa muu olio, generaattori tai resurssi undefined

Taulukot kopioidaan: niiden muutokset, jotka JS tekee, eivät näy PHP-taulukossa. Tavallisia PHP-olioita ei vielä voi välittää (ongelma #18); katso kiertotapoja kohdasta Jaetut oliot.

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

Toiminnon JS sisällä:

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

kutsu

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

Kutsuu globaalia JS-funktiota, jolle välitetään PHP-argumentit (muunnettuina kuten kohdassa setGlobal). Palautusarvo muunnetaan takaisin tyypiksi PHP. Määrittelemättömän nimen kutsuminen heittää virheen Phasis\Exceptions\ReferenceError; funktiona toimimattoman globaalin muuttujan kutsuminen heittää virheen Phasis\Exceptions\TypeError.

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

Konsolituloste

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

console.log, console.info, console.warn, console.error ja console.debug eivät tulosta mitään. Kukin kutsu lisää moottorin puskuriin yhden muotoillun rivin. getConsoleLines() palauttaa puskuroidut rivit merkkijonoluettelona, getConsoleOutput() palauttaa ne arvolla \n yhdistettyinä, ja clearConsole() tyhjentää puskurin. Puskuria ei tyhjennetä eval()-kutsujen välillä.

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

Asettaa suorituksen aikana muutettavan resurssirajan. Niitä on tasan yksi:

Raja Oletusarvo Kuvaus
maxLoopIterations 100000 puuta käyvän tulkin suorittamien for / while / do…while -silmukoiden enimmäisiterointimäärä
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Kaikki muut nimet ohitetaan hiljaisesti; merkkijonon pituudelle, konsolitulosteelle tai seinäkelloajalle ei ole rajoja.

Silmukkarajaa ei vielä valvota kaikkialla. Tavutavun virtuaalikoneessa suoritettavia silmukoita, eli funktioiden rungot ja tavukoodikääntäjän hyväksymät ylimmän tason ohjelmat, ei lasketa lainkaan, joten while (true) {} suoritetaan, kunnes PHP pysäyttää sen (ongelma #19). Älä luota siihen epäluotetun koodin pysäyttämisessä; käytä PHP-ohjelman max_execution_time-toimintoa tai erillistä prosessia.

Rajan ylittyessä virheen voi siepata JS try / catch -rakenteella (virheenä Error, jonka viesti on "Silmukan iteraatioiden enimmäismäärä ylitetty"). Jos mikään ei sieppaa virhettä, se välittyy arvoon PHP muodossa Phasis\Exceptions\InternalError, joka perii luokan Phasis\Exceptions\RuntimeError, ei luokkaa JsThrowable.

Moottoriin on kovakoodattu kaksi lisärajaa, joita ei voi muuttaa suorituksen aikana: Phasis\Runtime\CallStack::maxDepth (4,096 JS kehystä) ja JsToPhp-muunnettujen sulkeumien syvyysraja (8,192 PHP kehystä). Kummankin ylittyessä heitetään virhe "Maximum call stack size exceeded", joka välittyy myös arvoon PHP muodossa InternalError. Rajat ovat selvästi suurempia kuin minkään kelvollisen JS-ohjelman tarpeet — niiden kasvattaminen vaatii koodimuutoksen, eikä onnistu asetuksella.

nollaa

PHP
public function reset(): void

Tyhjentää kaikki käyttäjän määrittämät globaalit muuttujat ja konsolipuskurin sekä rakentaa vakiokirjaston ja tapahtumasilmukan uudelleen.

Oletusarvoisessa laiskassa tilassa rakennetusta moottorista reset() poistaa tällä hetkellä kaikki laiskasti asennetut sisäänrakennetut toiminnot: sen jälkeen Map, JSON, Math, console, fetch ja muut ovat määrittelemättömiä (ongelma #22). Kunnes tämä on korjattu, rakenna sen sijaan uusi Engine tai rakenna se käyttäen asetusta eager: true, jolloin reset() toimii.

Tapahtumasilmukka

Moottorin mukana toimitetaan JavaScript-tapahtumasilmukka, jossa globaalissa oliossa ovat setTimeout, setInterval, clearTimeout, clearInterval ja queueMicrotask. Mikrotehtävät (Promise-käsittelijät, queueMicrotask) käsitellään jokaisen makrotehtävän välillä; ajastimet laukeavat määräaikajärjestyksessä aluekohtaisen tehtäväjonon kautta.

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

eval() ja evalAsModule() kutsuvat runEventLoop()-toimintoa automaattisesti synkronisen koodin suorituksen jälkeen. Siksi ylimmällä tasolla ajastuksen setTimeout(cb, 5000) asettava skripti suorittaa sen ennen kuin eval() palauttaa arvon cb — silmukka käyttää virtuaaliaikaa ja siirtää kelloaan seuraavaan odottavaan määräaikaan sen sijaan, että odottaisi PHP. Vastaa node script.js-semantiikkaa.

Reaaliaikaista upotusta varten (pitkäikäiset palvelimet, UI-integraatio, aikataulutus todellisen kellon mukaan) tickEventLoop() suorittaa yhden iteraation (käsittelee mikrotehtävät ja laukaisee erääntyneet ajastimet) ja palauttaa ohjauksen. pendingTaskCount() ilmoittaa odottavien ajastimien määrän; upottaja voi ajaa omaa silmukkaansa:

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

Karkaava setInterval on rajattu: runEventLoop() pysähtyy 100,000 syklin jälkeen ja heittää tavallisen \RuntimeException-virheen — koodin JS vika näkyy selvänä PHP-virheenä jumiutumisen sijasta. Aikaväli pysyy ajastettuna, joten jokainen myöhempi eval() samassa moottorissa heittää saman virheen; hylkää moottori sen jälkeen. Ajastinkutsun takaisinkutsussa sieppaamattomat virheet ilmoitetaan toiminnon error_log() kautta, ja silmukka jatkaa tehtävien käsittelyä; yksi virheellinen ajastin ei pysäytä ohjelmaa.

Fetch-koukut

Phasis sisältää oletusarvoisesti käytössä olevan toiminnon fetch(), jonka kuljetuskerroksena toimii PHP-ohjelman ext-curl. Kolmen koukun avulla upottaja voi vaihtaa kuljetustapaa, rajoittaa pyyntöjä tai ottaa evästeiden tallennuksen käyttöön. Kaikki kolme hyväksyvät joko PHP-kutsuttavan tai olion metodin; arvorakennetta koskevat vaatimukset ovat kohdassa Web-rajapinnat.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Korvaa oletusarvoisen CurlTransport-kuljetuksen. Kutsuttava saa pyyntökuvauksen ja valinnaisen AbortSignal JsObject -olion; se palauttaa vastauskuvauksen.

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

Yleinen upotusmalli: asenna testeihin valekuljetus ja tuotantoon oikea curl-kuljetus.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Ennakkotarkistuksen käytäntökoukku. Saa arvon Request JsObject; voi heittää virheen pyyntöä estääkseen, palauttaa muokatun arvon Request pyynnön muuttamiseksi tai palauttaa arvon null tai olla palauttamatta mitään pyynnön sallimiseksi.

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

Valinnaisesti käyttöönotettava evästevarasto. Hyväksyy minkä tahansa olion, joka tarjoaa metodit get(string $url): string ja set(string $url, string $header): void — joko PHP-puolella tai JS-puolella.

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

Ilman arvoa setCookieJar() fetch ohittaa sekä saapuvat Set-Cookie-otsakkeet että lähtevät Cookie-otsakkeet — oletuksena evästeitä ei käytetä.

Poikkeukset

Luokka Perii luokan Milloin
Phasis\Exceptions\JsThrowable RuntimeError eksplisiittinen JS throw, jota mikään ei siepannut
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError moottorin itse aiheuttama virhe (esimerkiksi null.x tai määrittelemätön muuttuja), jota mikään ei siepannut
Phasis\Exceptions\InternalError RuntimeError silmukkaraja tai kutsupinon enimmäisraja ylittyi
Phasis\Exceptions\RuntimeError \RuntimeException edellä mainittujen luokkien kantaluokka
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) lähdekoodin jäsentäminen epäonnistuu
\RuntimeException execFile() ei pysty lukemaan tiedostoa; tapahtumasilmukka saavutti 100,000 syklin rajansa

Skriptiä varten kannattaa siepata kaikki virheet sieppaamalla Phasis\Exceptions\RuntimeError ja Phasis\Exceptions\SyntaxError tai yksinkertaisesti \RuntimeException.

Omien PHP-isäntäfunktioidesi heittämät poikkeukset käsitellään eri tavalla: ne kulkevat tällä hetkellä rakenteiden JS try / catch läpi ja päätyvät muuttumattomina toiminnon eval() kutsujalle säilyttäen alkuperäisen luokkansa (ongelma #20). Katso Isäntäfunktiot.

Elinkaari

Engine on nopea rakentaa (kylmäkäynnistys kestää muutamia millisekunteja), ja vakiokirjasto asennetaan laiskasti. Useimmissa upotustilanteissa yksi pitkään käytössä oleva moottori prosessia kohden riittää. Usean vuokralaisen käytössä anna kullekin vuokralaiselle tai pyynnölle oma moottori, jotta globaalit muuttujat eivät vuoda niiden välillä. Uuden moottorin rakentaminen antaa tällä hetkellä luotettavimmin puhtaan aloitustilan (katso nollaa).

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