Gå til indhold

API

EksekveringsAPI er én klasse: Phasis\Engine. Alt andet (JsValue, JsFunction, JsObject, …) er internt i motoren og eksponeres kun for værtsfunktioner, der forbinder PHP↔JS. Den eneste anden offentlige grænseflade er den selvstændige parser API (klasserne Phasis\Parser\Parser og Phasis\Ast), som parser kildekode uden at eksekvere 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 stiller hele standardbiblioteket til rådighed — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error og sprogets grundlæggende intrinsics installeres med det samme, så specifikationen virker (klasser kan ikke bruges uden Function.prototype, async/await uden Promise eller regex-literal uden RegExp). Alt andet — Math, JSON, Map, Set, Date, TypedArray og array-buffer-familien, Atomics, Proxy, Reflect, console, Intl, weak-ref-familien, Temporal, ShadowRealm, Web Platform Pack og Fetch Pack — registreres som en pladsholderadgangsfunktion og materialiseres ved første læsning. Læsning af globalen er gennemsigtig: Map, typeof Map, 'Map' in globalThis og Object.keys(globalThis).includes('Map') fungerer alle, og efter første læsning erstattes pladsholderen af en almindelig datadeskriptor.

Angiv eager: true, hvis du specifikt har brug for adfærden før lazy-indlæsning — alle indbyggede funktioner installeres ved konstruktion, og hele deres deskriptorflade er synlig ved introspektion. Overensstemmelsestestrammen (bin/test262, bin/wpt) opretter motorer i eager-tilstand, så test262/WPT ser de deskriptorformer, de forventer.

eval

PHP
public function eval(string $source): mixed

Parser og eksekverer $source som et klassisk script i motorens globale scope og kører derefter hændelsesløkken, indtil den er tom. Returnerer værdien af det sidst evaluerede udtryk konverteret til PHP (eller null, hvis programmet ikke havde et afsluttende udtryk). Se Værdikonvertering for reglerne.

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)

Et Promise pakkes ikke ud: eval('Promise.resolve(5)') returnerer et tomt array. Da hændelsesløkken er afviklet, når eval() returnerer, skal den afsluttede værdi gemmes i en global variabel og læses med endnu et eval():

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

En eksplicit JS throw, som intet fanger, vises som Phasis\Exceptions\JsThrowable med meddelelsen "Name: message", og egenskaben $jsValue indeholder den udløste JsValue. Fejl, som motoren selv udløser, f.eks. ved læsning af en egenskab på null eller en udefineret variabel, vises i stedet som Phasis\Exceptions\TypeError, ReferenceError eller RangeError. Alle disse nedarver fra Phasis\Exceptions\RuntimeError; parsefejl nedarver fra Phasis\Exceptions\SyntaxError. Se Undtagelser.

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

Evaluerer $source som et ES-modul: import / export, await på øverste niveau og import.meta er tilgængelige, og relative specifikationer løses i forhold til $path (en absolut filsti). Hændelsesløkken køres som ved eval(), og der returneres altid null; læs resultater via globaler eller konsolbufferen.

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

Indlæseren løser kun relative og absolutte stier; se Kendte begrænsninger.

execFile

PHP
public function execFile(string $path): mixed

Læser en fil fra disken og sender indholdet til eval(), så den køres som et klassisk script uanset filtypenavnet; en fil med import / export fejler med en SyntaxError. Filens faktiske sti bliver udgangspunkt for dynamiske import()-specifikationer. Brug evalAsModule() til modulfiler.

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

Hvis filen ikke kan læses, udsender PHP en advarsel, og execFile() udløser en almindelig \RuntimeException ("Cannot read file: …").

setGlobal

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

Binder en global variabel, som er synlig ved alle efterfølgende JS-evalueringer. PHP-værdier konverteres til JS:

PHP-type JS-type
null null
bool boolean
int / float number
string string (også når den navngiver en funktion, som 'strlen')
array (liste) Array (en kopi)
array (associativt) Object (en kopi)
Closure, kaldeligt objekt, [$object, 'method'] function
ethvert andet objekt, generator eller ressource undefined

Arrays kopieres: ændringer, som JS foretager i dem, ses ikke i PHP-arrayet. Almindelige PHP-objekter kan endnu ikke sendes (issue #18); se Delte objekter for løsninger.

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

Inde i JS:

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

call

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

Kalder en global JS-funktion med argumenterne PHP (konverteret som ved setGlobal). Returværdien konverteres tilbage til PHP. Kald af et navn, der ikke er defineret, udløser Phasis\Exceptions\ReferenceError; kald af en global, der ikke er en funktion, udløser Phasis\Exceptions\TypeError.

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

Konsoloutput

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

console.log, console.info, console.warn, console.error og console.debug udskriver ikke noget. Hvert kald tilføjer en formateret linje til en buffer på motoren. getConsoleLines() returnerer de bufrede linjer som en liste af strenge, getConsoleOutput() returnerer dem samlet med \n, og clearConsole() tømmer bufferen. Bufferen ryddes ikke mellem eval()-kald.

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

Angiver en justerbar ressourcegrænse under kørsel. Der findes præcis én:

Grænse Standardværdi Beskrivelse
maxLoopIterations 100000 maksimalt antal iterationer pr. for / while / do…while-løb i tree-walking-fortolkeren
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Alle andre navne ignoreres uden varsel; der er ingen grænser for strenglængde, konsoloutput eller køretid.

Løkkegrænsen håndhæves endnu ikke overalt. Løkker, der kører på bytekode-VM'en, som omfatter funktionskroppe og programmer på øverste niveau, som bytekodekompileren accepterer, tælles aldrig, så while (true) {} kører, indtil PHP stopper den (issue #19). Stol ikke på, at den stopper ikke-betroet kode; brug PHP's max_execution_time eller en separat proces.

Når grænsen nås, kan fejlen fanges af en JS try / catch (som en Error med meddelelsen "Maximum loop iterations exceeded"). Hvis intet fanger den, når den PHP som Phasis\Exceptions\InternalError, der nedarver fra Phasis\Exceptions\RuntimeError, ikke fra JsThrowable.

To yderligere grænser er indbygget i motoren og kan ikke ændres under kørsel: Phasis\Runtime\CallStack::maxDepth (4,096 JS frames) og JsToPhp-transpilerens dybdegrænse for transpilerede closures (8,192 PHP frames). Overskrides en af dem, udløses "Maximum call stack size exceeded", som også når frem til PHP som InternalError. De ligger langt over ethvert legitimt JS-program — at hæve dem kræver en kodeændring, ikke en konfigurationsindstilling.

nulstil

PHP
public function reset(): void

Rydder alle brugerdefinerede globale værdier og konsolbufferen og genopbygger standardbiblioteket og hændelsesløkken.

På en motor, der er konstrueret i standardtilstanden lazy, mister reset() i øjeblikket alle lazily installerede indbyggede funktioner: bagefter er Map, JSON, Math, console, fetch og resten udefinerede (issue #22). Indtil det er rettet, skal du i stedet konstruere en ny Engine eller konstruere med eager: true, hvor reset() fungerer.

Hændelsesløkke

Motoren leveres med en JavaScript-hændelsesløkke med setTimeout, setInterval, clearTimeout, clearInterval og queueMicrotask på det globale objekt. Mikrotasks (Promise-handlere, queueMicrotask) afvikles mellem hver makrotask; timere udløses i fristorden via en opgavekø pr. realm.

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

eval() og evalAsModule() kalder automatisk runEventLoop(), når den synkrone kode er færdig. Et script, der planlægger setTimeout(cb, 5000) på topniveau, afvikler derfor cb, før eval() returnerer — løkken bruger virtuel tid og springer uret frem til den næste ventende frist i stedet for at sove PHP. Svarer til node script.js-semantik.

Til indlejring i realtid (langlivede servere, UI-integration, planlægning efter et rigtigt ur) kører tickEventLoop() én iteration (afvikler mikrotasks og udløser eventuelle forfaldne timere) og returnerer. pendingTaskCount() rapporterer ventende timere; en indlejrer kan selv styre løkken:

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

En løbsk setInterval er begrænset: runEventLoop() stopper efter 100,000 cyklusser og udløser en almindelig \RuntimeException — fejlen i JS-koden vises som en tydelig PHP-fejl i stedet for et hæng. Intervallet forbliver planlagt, så alle senere eval() på samme motor udløser den samme fejl; kassér motoren bagefter. Ikke-indfangede fejl i et timer-callback sendes via error_log(), og løkken fortsætter afviklingen; én fejlbehæftet timer stopper ikke programmet.

Fetch-hooks

Phasis leveres med fetch() aktiveret som standard og bruger ext-curl fra PHP som underliggende transport. Tre hooks lader en indlejrer udskifte transporten, styre anmodninger eller aktivere lagring af cookies. Alle tre accepterer enten en callable af typen PHP eller en metode på et objekt; se Web-API'er for kontrakten for værdiformer.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Erstatter standardtransporten CurlTransport. Callable-funktionen modtager en anmodningsbeskrivelse og et valgfrit AbortSignal JsObject og returnerer en svarbeskrivelse.

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

Et almindeligt mønster ved indlejring: installér en mock-transport i tests og den rigtige curl-transport i produktion.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Hook til politik før afsendelse. Modtager Request JsObject; kan udløse en fejl for at afvise, returnere en ændret Request for at omskrive eller returnere null/ingen værdi for at tillade.

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

Valgfrit cookielager. Accepterer ethvert objekt, der eksponerer get(string $url): string og set(string $url, string $header): void — på PHP-siden eller JS-siden.

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

Uden setCookieJar() ignorerer fetch både indgående Set-Cookie-headere og udgående Cookie-headere — standarden er ingen cookies.

Undtagelser

Klasse Nedstammer fra Hvornår
Phasis\Exceptions\JsThrowable RuntimeError en eksplicit JS throw, som intet fangede
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError en fejl, som motoren selv udløste (f.eks. null.x eller en udefineret variabel), og som intet fangede
Phasis\Exceptions\InternalError RuntimeError løkkegrænsen eller kaldestakgrænsen blev overskredet
Phasis\Exceptions\RuntimeError \RuntimeException basisklasse for ovenstående
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) kilden kan ikke parses
\RuntimeException execFile() kan ikke læse filen; hændelsesløkken nåede sin grænse på 100,000 cyklusser

Hvis du vil fange alle fejl i et script, skal du fange Phasis\Exceptions\RuntimeError og Phasis\Exceptions\SyntaxError eller blot \RuntimeException.

Undtagelser udløst af dine egne PHP-værtsfunktioner er anderledes: De passer i øjeblikket uændret gennem JS try / catch og når frem til den, der kalder eval(), med deres oprindelige klasse (issue #20). Se Værtsfunktioner.

Livscyklus

Engine er hurtig at konstruere (~få ms ved koldstart), og standardbiblioteket installeres lazy. I de fleste situationer med indlejring er én langlivet motor pr. proces tilstrækkelig. Ved brug med flere lejere skal hver lejer eller anmodning have sin egen motor, så globale værdier ikke lækker mellem dem. Den pålidelige måde at starte på en ren tavle i dag er at konstruere en ny motor (se nulstil).

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