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
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
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.
$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():
$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.
try {
$engine->eval('throw new Error("boom")');
} catch (\Phasis\Exceptions\JsThrowable $e) {
echo $e->getMessage();
// Error: boom
}
evalAsModule
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.
$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
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.
$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
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.
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
Inuti JS:
console.log(config.version); // "1.0"
log('hello'); // calls the PHP closure
call
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.
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3); // 5
Konsolutdata
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().
$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
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 |
$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
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.
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:
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
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.
// 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
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.
// 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
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.
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).
foreach ($requests as $req) {
$engine = new Engine();
$engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
$engine->execFile($req->scriptPath);
}