Hoppa till innehåll

API

Körningsytan API består av en enda klass: Phasis\Engine. Allt annat (JsValue, JsFunction, JsObject, …) är internt i motorn och exponeras bara för värdfunktioner som överbryggar PHP↔JS. Den enda andra publika ytan är den fristående parsern API (Phasis\Parser\Parser och Phasis\Ast-klasserna), som tolkar källkod utan att köra den.

Konstruktion

PHP
use Phasis\Engine;

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

En ny Engine exponerar hela standardbiblioteket — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error och språkets grundläggande inbyggda funktioner installeras omedelbart så att specifikationen fungerar (du kan inte ha klasser utan Function.prototype, async/await utan Promise eller regexliteraler utan RegExp). Allt annat — Math, JSON, Map, Set, Date, TypedArray och array-buffer-familjen, Atomics, Proxy, Reflect, console, Intl, weak-ref-familjen, Temporal, ShadowRealm, Web Platform Pack och Fetch Pack — registreras som en platshållaraccessor och materialiseras vid första läsningen. Läsning av globala objektet är transparent: Map, typeof Map, 'Map' in globalThis och Object.keys(globalThis).includes('Map') fungerar alla, och efter första läsningen ersätts platshållaren med en vanlig datadeskriptor.

Ange eager: true om du specifikt behöver beteendet före lazy-installationen — alla inbyggda funktioner installeras vid konstruktionen och hela deskriptorgränssnittet är synligt vid introspektion. Konformitetstestriggen (bin/test262, bin/wpt) konstruerar motorer i eager-läge så att test262/WPT ser de deskriptorformer de förväntar sig.

eval

PHP
public function eval(string $source): mixed

Tolkar och kör $source som ett klassiskt skript i motorns globala omfång och kör sedan händelseloopen tills den är tom. Returnerar värdet från det senast utvärderade uttrycket konverterat till PHP (eller null om programmet saknade ett avslutande uttryck). Se Konvertering av värden för reglerna.

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)

Ett Promise packas inte upp: eval('Promise.resolve(5)') returnerar en tom array. Eftersom händelseloopen har tömts när eval() returnerar lagrar du det lösta värdet i en global variabel och läser det med ett andra anrop till eval():

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

Ett uttryckligt JS throw som ingen fångar upp visas som Phasis\Exceptions\JsThrowable, vars meddelande är "Name: message" och vars egenskap $jsValue innehåller det kastade JsValue. Fel som motorn själv orsakar, till exempel vid läsning av en egenskap hos null eller en odefinierad variabel, visas i stället som Phasis\Exceptions\TypeError, ReferenceError eller RangeError. Alla dessa utökar Phasis\Exceptions\RuntimeError; tolkningsfel utökar Phasis\Exceptions\SyntaxError. Se Undantag.

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

Utvärderar $source som en ES-modul: import / export, toppnivå-await och import.meta är tillgängliga, och relativa specifikatorer löses mot $path (en absolut filsökväg). Kör händelseloopen som eval() och returnerar alltid null; läs resultat via globala variabler eller konsolbufferten.

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

Laddaren löser bara relativa och absoluta sökvägar; se Kända begränsningar.

execFile

PHP
public function execFile(string $path): mixed

Läser en fil från disken och skickar innehållet till eval(), så den körs som ett klassiskt skript oavsett filändelse. En fil med import / export ger ett SyntaxError. Filens verkliga sökväg blir basen för dynamiska import()-specifikatorer. Använd evalAsModule() för modulfiler.

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

Om filen inte kan läsas avger PHP en varning och execFile() kastar ett vanligt \RuntimeException ("Kan inte läsa filen: …").

setGlobal

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

Binder en global variabel som är synlig för alla efterföljande JS-utvärderingar. PHP-värden konverteras till JS:

PHP-typ JS-typ
null null
bool boolean
int / float number
string string (även när det är namnet på en funktion, till exempel 'strlen')
array (lista) Array (en kopia)
array (associativ) Object (en kopia)
Closure, anropbart objekt, [$object, 'method'] function
annat objekt, generator eller resurs undefined

Arrayer kopieras: ändringar som JS gör i dem syns inte i PHP-arrayen. Vanliga PHP-objekt kan ännu inte skickas (problem #18); se Delade objekt för lösningar.

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

Inuti JS:

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

call

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

Anropar en global JS-funktion med PHP-argument (konverterade som för setGlobal). Returvärdet konverteras tillbaka till PHP. Anrop med ett odefinierat namn kastar Phasis\Exceptions\ReferenceError; anrop av en global variabel som inte är en funktion kastar Phasis\Exceptions\TypeError.

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

Konsolutdata

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

console.log, console.info, console.warn, console.error och console.debug skriver inte ut något. Varje anrop lägger till en formaterad rad i en buffert i motorn. getConsoleLines() returnerar de buffrade raderna som en lista med strängar, getConsoleOutput() returnerar dem sammanfogade med \n och clearConsole() tömmer bufferten. Bufferten rensas inte mellan anrop till eval().

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

Anger en resursgräns som kan ändras under körning. Det finns exakt en:

Gräns Standardvärde Beskrivning
maxLoopIterations 100000 maximalt antal iterationer per for- / while- / do…while-loopkörning i den trädvandrade tolken
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Alla andra namn ignoreras i tysthet. Det finns inga gränser för stränglängd, konsolutdata eller körtid i realtid.

Loopgränsen tillämpas ännu inte överallt. Loopar som körs på bytekodens VM, vilket omfattar funktionskroppar och program på toppnivå som bytekodskompilatorn godtar, räknas aldrig. Därför kör while (true) {} tills PHP stoppar den (problem #19). Förlita dig inte på den för att stoppa opålitlig kod; använd PHPs max_execution_time eller en separat process.

När gränsen nås kan felet fångas av en JS try / catch (som ett Error med meddelandet "Maximum loop iterations exceeded"). Om ingen fångar upp det når det PHP som Phasis\Exceptions\InternalError, vilket utökar Phasis\Exceptions\RuntimeError, inte JsThrowable.

Två ytterligare tak är inbyggda i motorn och kan inte ändras under körning: Phasis\Runtime\CallStack::maxDepth (4,096 JS ramar) och djupvakten för transpilerade JsToPhp-closures (8,192 PHP ramar). Om något av taken överskrids kastas "Maximum call stack size exceeded", som också når PHP som InternalError. Gränserna ligger långt över vad legitima JS-program behöver — att höja dem kräver en kodändring, inte en konfigurationsinställning.

reset

PHP
public function reset(): void

Rensar alla användardefinierade globala variabler och konsolbufferten samt bygger om standardbiblioteket och händelseloopen.

I en motor som skapats i standardläget lazy förlorar reset() för närvarande alla inbyggda funktioner som installerats lazy. Efteråt är Map, JSON, Math, console, fetch och resten odefinierade (problem #22). Tills det är åtgärdat bör du i stället skapa en ny Engine eller skapa den med eager: true, där reset() fungerar.

Händelseloop

Motorn levereras med en JavaScript-händelseloop med setTimeout, setInterval, clearTimeout, clearInterval och queueMicrotask på det globala objektet. Mikrotasks (Promise-hanterare, queueMicrotask) töms mellan varje makrotask. Timers utlöses i ordning efter deadline via en uppgiftskö per realm.

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

eval() och evalAsModule() anropar automatiskt runEventLoop() när den synkrona koden har slutförts. Ett skript som schemalägger setTimeout(cb, 5000) på toppnivå kör därför cb innan eval() returnerar — loopen använder virtuell tid och flyttar klockan fram till nästa väntande deadline i stället för att vänta PHP. Följer semantiken i node script.js.

För inbäddning i realtid (servrar som kör länge, UI-integration och schemaläggning mot en verklig klocka) kör tickEventLoop() en iteration (tömmer mikrotasks och utlöser timers som förfallit) och returnerar. pendingTaskCount() rapporterar väntande timers. Den som bäddar in motorn kan driva sin egen pump:

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

En skenande setInterval begränsas: runEventLoop() sätter ett tak på 100,000 cykler och kastar ett vanligt \RuntimeException — felet i JS-koden visas som ett tydligt PHP i stället för att orsaka ett låst läge. Intervallet fortsätter att vara schemalagt, så varje senare eval() på samma motor kastar samma fel. Kassera motorn efteråt. Ohanterade fel i en timer-callback skickas via error_log() och loopen fortsätter att tömma kön. En timer med fel stoppar inte programmet.

Fetch-hooks

Phasis levereras med fetch() aktiverat som standard och PHPs ext-curl som underliggande transport. Tre hooks låter den som bäddar in motorn byta transport, begränsa förfrågningar eller aktivera lagring av cookies. Alla tre godtar antingen en anropsbar PHP eller en metod på ett objekt. Se Webb-API:er för reglerna för värdeformer.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Ersätter standardtransporten CurlTransport. Den anropsbara funktionen tar emot en förfrågningsbeskrivning och ett valfritt AbortSignal JsObject och returnerar en svarsbeskrivning.

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

Ett vanligt mönster vid inbäddning: installera en mocktransport i tester och den riktiga curl-transporten i produktion.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Policy-hook som körs före förfrågan. Tar emot Request JsObject; kan kasta ett undantag för att neka, returnera ett ändrat Request för att skriva om eller returnera null/inget värde för att tillåta.

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

Valfri cookie-lagring. Godtar alla objekt som exponerar get(string $url): string och set(string $url, string $header): void — på PHP-sidan eller JS-sidan.

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

Utan setCookieJar() ignorerar fetch både inkommande Set-Cookie- och utgående Cookie-headers — standardinställningen är inga cookies.

Undantag

Klass Utökar När
Phasis\Exceptions\JsThrowable RuntimeError ett uttryckligt JS throw som ingen fångade upp
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError ett fel som motorn själv orsakat (till exempel null.x eller en odefinierad variabel) och som ingen fångade upp
Phasis\Exceptions\InternalError RuntimeError loopgränsen eller taket för anropsstacken överskreds
Phasis\Exceptions\RuntimeError \RuntimeException basklass för klasserna ovan
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) källkoden kan inte tolkas
\RuntimeException execFile() kan inte läsa filen; händelseloopen nådde sitt tak på 100,000 cykler

För att fånga alla fel i ett skript, fånga både Phasis\Exceptions\RuntimeError och Phasis\Exceptions\SyntaxError, eller använd bara \RuntimeException.

Undantag som kastas av dina egna värdfunktioner i PHP skiljer sig åt: de passerar för närvarande genom JS try / catch och når anroparen av eval() oförändrade, med sin ursprungliga klass (problem #20). Se Värdfunktioner.

Livscykel

Engine är snabb att konstruera (~några ms kallstart) och standardbiblioteket installeras lazy. För de flesta inbäddningsscenarier fungerar en enda långlivad motor per process bra. Vid användning för flera klienter bör varje klient eller förfrågan få en egen motor så att globala variabler aldrig läcker mellan dem. Att konstruera en ny motor är det tillförlitliga sättet att börja om rent i dag (se reset).

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