Ugrás a tartalomhoz

API

A végrehajtási API egyetlen osztály: Phasis\Engine. Minden más (JsValue, JsFunction, JsObject, …) a motor belső része, és csak a PHP↔JS közötti kapcsolatot biztosító host-függvények számára érhető el. Az egyetlen másik nyilvános felület az önálló parser API (Phasis\Parser\Parser és a Phasis\Ast osztályok), amely végrehajtás nélkül elemzi a forrást.

Létrehozás

PHP
use Phasis\Engine;

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

Egy új Engine a teljes standard könyvtárat teszi elérhetővé — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error és a nyelv alapvető beépített elemei azonnal települnek, hogy működjön a specifikáció (osztályok nem létezhetnek Function.prototype nélkül, az async/await az Promise nélkül, a reguláris kifejezések literáljai pedig az RegExp nélkül). Minden más — Math, JSON, Map, Set, Date, TypedArray és a tömbpuffer-család, Atomics, Proxy, Reflect, console, Intl, a gyenge hivatkozások családja, Temporal, ShadowRealm, a Web Platform Pack és a Fetch Pack — helyőrző hozzáférőként van regisztrálva, és az első olvasáskor jön létre. A globális változó olvasása átlátható: a Map, typeof Map, 'Map' in globalThis és Object.keys(globalThis).includes('Map') mind működik, az első olvasás után pedig a helyőrzőt normál adatleíró váltja fel.

Adja meg a eager: true értéket, ha kifejezetten az előzetes lusta betöltéses viselkedésre van szüksége — azaz minden beépített elem már a létrehozáskor települ, és a teljes leírófelület látható az introspekció számára. A megfelelőségi tesztfuttató (bin/test262, bin/wpt) előre betöltött módban hozza létre a motorokat, hogy a test262/WPT a várt leíróalakokat lássa.

eval

PHP
public function eval(string $source): mixed

A $source elemet klasszikus parancsfájlként elemzi és futtatja a motor globális hatókörében, majd addig futtatja az eseményhurkot, amíg ki nem ürül. Az utolsó kiértékelt kifejezés értékét PHP formátumban adja vissza (vagy null értéket, ha a program végén nem volt kifejezés). A szabályokat lásd az Értékátalakítás oldalon.

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)

A Promise nincs feloldva: a eval('Promise.resolve(5)') üres tömböt ad vissza. Mivel az eseményhurok leáll, mire a eval() visszatér, tárolja az elszámolt értéket egy globális változóban, majd olvassa ki egy második eval() hívással:

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

A kifejezett JS throw, amelyet semmi sem kap el, Phasis\Exceptions\JsThrowable formájában jelenik meg; az üzenete "Name: message", a $jsValue tulajdonsága pedig a kiváltott JsValue értéket tartalmazza. A motor által maga előidézett hibák, például egy null tulajdonságának olvasása vagy egy definiálatlan változó használata ehelyett Phasis\Exceptions\TypeError, ReferenceError vagy RangeError formájában jelennek meg. Ezek mindegyike a Phasis\Exceptions\RuntimeError osztályból származik; az elemzési hibák a Phasis\Exceptions\SyntaxError osztályból. Lásd: Kivételek.

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

A $source értéket ES-modulként értékeli ki: a import / export, a felső szintű await és import.meta elérhető, a relatív specifikátorok pedig a $path alapján oldódnak fel (abszolút fájlútvonal). Az eseményhurkot az eval() módjára futtatja, és mindig null értéket ad vissza; az eredményeket globális változókon vagy a konzolpufferen keresztül olvashatja ki.

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

A betöltő csak a relatív és abszolút útvonalakat oldja fel; lásd: Ismert korlátozások.

execFile

PHP
public function execFile(string $path): mixed

Beolvassa a fájlt a lemezről, és tartalmát átadja a eval() számára, így a kiterjesztéstől függetlenül klasszikus parancsfájlként fut le; a import / export elemet tartalmazó fájl SyntaxError hibát okoz. A fájl valódi elérési útja lesz a dinamikus import() specifikátorok alapja. Modulfájlokhoz használja a evalAsModule() elemet.

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

Ha a fájl nem olvasható, a PHP figyelmeztetést jelenít meg, a execFile() pedig egyszerű \RuntimeException hibát dob ("Cannot read file: …").

setGlobal

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

Globális változót köt be, amely az összes későbbi JS kiértékelésben látható. A PHP értékek JS formátumra alakulnak:

PHP típusa JS típusa
null null
bool boolean
int / float number
string string (akkor is, ha függvény neve, például 'strlen')
tömb (lista) Array (másolat)
tömb (asszociatív) Object (másolat)
Closure, hívható objektum, [$object, 'method'] function
bármely más objektum, generátor vagy erőforrás undefined

A tömbökről másolat készül: a JS által végrehajtott módosítások nem láthatók a PHP tömbben. Egyszerű PHP objektumokat még nem lehet átadni (18. számú hiba); a kerülőmegoldásokért lásd: Megosztott objektumok.

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

A JS belsejében:

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

call

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

Egy globális JS függvényt hív meg PHP argumentummal (az setGlobal szerinti átalakítással). A visszatérési érték PHP formátumra alakul. Ha olyan nevet hív meg, amely nincs definiálva, Phasis\Exceptions\ReferenceError hiba keletkezik; ha egy nem függvény globális változót hív meg, Phasis\Exceptions\TypeError hiba keletkezik.

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

Konzolkimenet

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

A console.log, console.info, console.warn, console.error és console.debug nem ír ki semmit. Minden hívás egy formázott sort fűz a motor pufferéhez. A getConsoleLines() listaként adja vissza a puffer sorait, a getConsoleOutput() a \n elválasztóval összefűzve adja vissza őket, a clearConsole() pedig kiüríti a puffert. A puffer nem ürül ki az egyes eval() hívások között.

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

Futásidőben módosítható erőforráskorlátot állít be. Pontosan egy ilyen korlát van:

Korlát Alapértelmezett érték Leírás
maxLoopIterations 100000 a fa-bejárásos értelmező által futtatott for / while / do…while ciklusok maximális iterációszáma
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Minden más nevet csendben figyelmen kívül hagy; nincs korlátozás a karakterlánc hosszára, a konzolkimenetre vagy a futásidőre.

A cikluskorlátot még nem mindenhol kényszeríti ki. A bytecode VM-en futó ciklusokat — amelyek a függvénytörzseket és a bytecode-fordító által elfogadott felső szintű programokat fedik le — nem számolja, ezért a while (true) {} addig fut, amíg a PHP le nem állítja (19. számú hiba). Ne hagyatkozzon erre nem megbízható kód leállításához; használja a PHP max_execution_time értékét vagy külön folyamatot.

A korlát elérésekor a hibát elkaphatja egy JS try / catch (Error néven, a "Maximum loop iterations exceeded" üzenettel). Ha semmi nem kapja el, PHP formájában jut el a Phasis\Exceptions\InternalError elemhez; ez a Phasis\Exceptions\RuntimeError osztályból származik, nem a JsThrowable osztályból.

A motorba két további, beépített és futásidőben nem állítható felső korlát került: Phasis\Runtime\CallStack::maxDepth (4,096 JS keret) és a JsToPhp által transzpilált lezárások mélységkorlátja (8,192 PHP keret). Túllépésükkor mindkettő „Maximum call stack size exceeded” hibát dob, amely PHP-ként, InternalError formájában is megjelenik. Jóval meghaladják a legitim JS programok igényeit — növelésük kódmódosítást igényel, nem konfigurációs beállítás.

Visszaállítás

PHP
public function reset(): void

Törli az összes felhasználó által definiált globális változót és a konzol pufferét, majd újraépíti a standard könyvtárat és az eseményhurkot.

Az alapértelmezett lusta módban létrehozott motorban a reset() jelenleg elveszíti az összes lustán telepített beépített elemet: utána Map, JSON, Math, console, fetch és a többi is definiálatlan lesz (issue #22). Amíg ezt nem javítják, inkább hozzon létre egy új Engine objektumot, vagy használja a eager: true beállítást, ahol a reset() működik.

Eseményhurok

A motor egy JavaScript eseményhurkot biztosít, amelyben a globális objektumon elérhető a setTimeout, setInterval, clearTimeout, clearInterval és a queueMicrotask. A mikrof feladatok (Promise-kezelők, queueMicrotask) minden makrof feladat között lefutnak; az időzítők határidő szerinti sorrendben aktiválódnak, realm-enkénti feladatsoron keresztül.

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

A eval() és a evalAsModule() automatikusan meghívja a runEventLoop() függvényt a szinkron kód befejeződése után. Ezért egy legfelső szinten setTimeout(cb, 5000) ütemező szkriptben a cb lefut, mielőtt a eval() visszatérne — a hurok virtuális időt használ: az alvás PHP helyett a következő függő határidőre állítja az órát. A node script.js szemantikáját követi.

Valós idejű beágyazás esetén (hosszan futó szerverek, UI-integráció, valós órához igazított ütemezés) a tickEventLoop() egyetlen iterációt futtat (kiüríti a mikrof feladatokat, aktiválja az esedékes időzítőket), majd visszatér. A pendingTaskCount() jelzi a még függő időzítőket; a beágyazó saját ciklust futtathat:

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

Egy elszabadult setInterval korlátozott: a runEventLoop() legfeljebb 100,000 ciklust futtat, majd egyszerű \RuntimeException hibát dob — az JS kód hibája egyértelmű PHP hibaként jelenik meg, nem a program lefagyásaként. Az időköz ütemezve marad, ezért ugyanazon a motoron minden későbbi eval() ugyanazt a hibát dobja; utána dobja el a motort. Az időzítő visszahívásában kezeletlen hibák a error_log() útján kerülnek kibocsátásra, az eseményhurok pedig folytatja a feladatok feldolgozását; egy hibás időzítő nem állítja le a programot.

Fetch-hookok

A Phasis alapértelmezés szerint engedélyezett fetch() funkcióval érkezik, amelynek mögöttes átviteli rétege a PHP ext-curl eleme. Három hookkal cserélheti le a beágyazó az átviteli réteget, szabályozhatja a kéréseket, illetve engedélyezheti a sütik tárolását. Mindhárom elfogad PHP hívható objektumot vagy objektumon lévő metódust; az értékek alakjáról lásd a Web APIs részt.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Az alapértelmezett CurlTransport lecserélése. A hívható objektum egy kérésleírót és egy opcionális AbortSignal JsObject objektumot kap; válasza egy válaszleíró.

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

Gyakori beágyazási minta: teszteléskor álátviteli réteget, éles környezetben pedig valódi curl átviteli réteget telepítenek.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Kérés előtti házirend-hook. Megkapja a Request JsObject objektumot; hibát dobhat a tiltáshoz, módosított Request értéket adhat vissza az átíráshoz, vagy null értéket, illetve semmit a kérés engedélyezéséhez.

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

Opcionálisan engedélyezhető sütitároló. Bármely olyan objektumot elfogad, amely rendelkezik a get(string $url): string és a set(string $url, string $header): void elemmel — PHP-oldali vagy JS-oldali megoldást.

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

setCookieJar() nélkül a fetch figyelmen kívül hagyja a bejövő Set-Cookie és a kimenő Cookie fejléceket — alapértelmezés szerint nincsenek sütik.

Kivételek

Osztály Kiterjeszti Mikor
Phasis\Exceptions\JsThrowable RuntimeError kifejezett JS throw esetén, amelyet semmi sem kapott el
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError a motor által kiváltott hiba esetén (például null.x vagy definiálatlan változó), amelyet semmi sem kapott el
Phasis\Exceptions\InternalError RuntimeError a hurokkorlát vagy a hívásverem korlátjának túllépésekor
Phasis\Exceptions\RuntimeError \RuntimeException a fentiek alaposztálya
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) a forráskód nem értelmezhető
\RuntimeException a execFile() nem tudja beolvasni a fájlt; az eseményhurok elérte a 100,000 ciklusos korlátját

A szkript minden hibájának elkapásához kapja el a Phasis\Exceptions\RuntimeError és a Phasis\Exceptions\SyntaxError hibákat, vagy egyszerűen a \RuntimeException osztályt.

A saját PHP hostfüggvényei által dobott kivételek másként működnek: jelenleg változatlanul jutnak át a JS try / catch rétegen, és az eredeti osztályukkal jutnak el a eval() hívójához (issue #20). Lásd: Host functions.

Életciklus

A Engine létrehozása gyors (hideg indításkor néhány ms), a standard könyvtár pedig lustán települ. A legtöbb beágyazási helyzetben folyamatonként elegendő egy hosszú életű motor. Több bérlős használat esetén minden bérlőhöz vagy kéréshez külön motort rendeljen, így a globális változók nem szivárognak át közöttük. Jelenleg új motor létrehozásával lehet megbízhatóan tiszta állapotból indulni (lásd: reset).

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