API
Jedinou třídou rozhraní pro spouštění API je Phasis\Engine. Všechno ostatní (JsValue, JsFunction, JsObject, …) je interní součástí enginu a zpřístupňují se pouze hostitelským funkcím, které propojují PHP↔JS. Jediným dalším veřejným rozhraním je samostatný parser API (třídy Phasis\Parser\Parser a Phasis\Ast), který parsuje zdrojový kód bez jeho spuštění.
Vytvoření
use Phasis\Engine;
$engine = new Engine(); // lazy mode (default)
$engine = new Engine(eager: true); // install every built-in up front
Nová instance Engine zpřístupňuje úplnou standardní knihovnu — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error i základní jazykové primitivy se instalují okamžitě, aby fungovala specifikace (bez Function.prototype nelze mít třídy, bez Promise async/await a bez RegExp regulární literály). Všechno ostatní — Math, JSON, Map, Set, Date, TypedArray a skupina array-buffer, Atomics, Proxy, Reflect, console, Intl, skupina weak-ref, Temporal, ShadowRealm, Web Platform Pack a Fetch Pack — se registruje jako zástupný přístupor a vytvoří se až při prvním čtení. Čtení globální proměnné je transparentní: fungují Map, typeof Map, 'Map' in globalThis i Object.keys(globalThis).includes('Map') a po prvním čtení zástupný objekt nahradí běžný deskriptor datové vlastnosti.
Pokud výslovně potřebujete chování před zavedením líného načítání — tedy instalaci všech vestavěných funkcí při vytvoření a plné rozhraní deskriptorů dostupné při introspekci — předejte eager: true. Testovací prostředí pro ověření shody (bin/test262, bin/wpt) vytváří enginy v režimu eager, aby test262/WPT viděly očekávané tvary deskriptorů.
eval
public function eval(string $source): mixed
Parsuje a spustí $source jako klasický skript v globálním oboru enginu a poté spouští smyčku událostí, dokud není prázdná. Vrátí hodnotu posledního vyhodnoceného výrazu převedenou na PHP (nebo null, pokud program nekončil výrazem). Pravidla najdete v části Převod hodnot.
$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)
Promise se nerozbalí: eval('Promise.resolve(5)') vrátí prázdné pole. Protože se smyčka událostí vyprázdní dřív, než se eval() vrátí, uložte vyřešenou hodnotu do globální proměnné a přečtěte ji druhým voláním eval():
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out'); // 9
Pokud explicitní JS throw nic nezachytí, projeví se jako Phasis\Exceptions\JsThrowable, jehož zpráva zní "Name: message" a vlastnost $jsValue obsahuje vyvolaný JsValue. Chyby vyvolané samotným enginem, například při čtení vlastnosti objektu null nebo nedefinované proměnné, se projeví jako Phasis\Exceptions\TypeError, ReferenceError nebo RangeError. Všechny tyto chyby dědí z Phasis\Exceptions\RuntimeError; chyby parsování dědí z Phasis\Exceptions\SyntaxError. Viz Výjimky.
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
Vyhodnotí $source jako modul ES: jsou dostupné import / export i nejvyšší úroveň await a import.meta; relativní specifikátory se vyhodnocují vzhledem k $path (absolutní cestě k souboru). Smyčku událostí spustí stejně jako eval() a vždy vrátí null; výsledky čtěte přes globální proměnné nebo vyrovnávací paměť konzole.
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
Zavaděč rozlišuje pouze relativní a absolutní cesty; známá omezení najdete v části Známá omezení.
execFile
public function execFile(string $path): mixed
Načte soubor z disku a předá jeho obsah eval(), takže se spustí jako klasický skript bez ohledu na příponu; soubor s import / export skončí chybou SyntaxError. Skutečná cesta k souboru se použije jako základ pro dynamické specifikátory import(). Pro soubory modulů použijte evalAsModule().
$engine->execFile(__DIR__ . '/script.js');
Pokud soubor nelze přečíst, PHP zobrazí varování a execFile() vyvolá běžnou chybu \RuntimeException („Nelze přečíst soubor: …“).
setGlobal
public function setGlobal(string $name, mixed $value): void
Naváže globální proměnnou viditelnou při všech následujících vyhodnoceních JS. Hodnoty PHP se převedou na JS:
| Typ PHP | Typ JS |
|---|---|
null |
null |
bool |
boolean |
int / float |
number |
string |
string (i když označuje funkci, například 'strlen') |
| pole (seznam) | Array (kopie) |
| pole (asociativní) | Object (kopie) |
Closure, volatelný objekt, [$object, 'method'] |
function |
| jakýkoli jiný objekt, generátor nebo prostředek | undefined |
Pole se kopírují: změny, které v nich provede JS, se v poli PHP neprojeví. Prosté objekty PHP zatím nelze předávat (problém #18); možnosti náhradního řešení najdete v části Sdílené objekty.
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
Uvnitř JS:
console.log(config.version); // "1.0"
log('hello'); // calls the PHP closure
call
public function call(string $name, mixed ...$args): mixed
Zavolá globální funkci JS s argumenty PHP (převedenými stejně jako v setGlobal). Návratová hodnota se převede zpět na PHP. Volání nedefinovaného názvu vyvolá Phasis\Exceptions\ReferenceError; volání globální proměnné, která není funkcí, vyvolá Phasis\Exceptions\TypeError.
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3); // 5
Výstup konzole
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
Volání console.log, console.info, console.warn, console.error a console.debug nic nevypisují. Každé volání přidá do vyrovnávací paměti enginu jeden formátovaný řádek. getConsoleLines() vrátí řádky z paměti jako seznam řetězců, getConsoleOutput() je vrátí spojené pomocí \n a clearConsole() paměť vyprázdní. Paměť se mezi voláními eval() nemaže.
$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
Nastaví limit prostředků, který lze měnit za běhu. Existuje právě jeden:
| Limit | Výchozí hodnota | Popis |
|---|---|---|
maxLoopIterations |
100000 |
maximální počet iterací při každém spuštění smyčky for / while / do…while v interpretu, který prochází strom |
$engine->setLimit('maxLoopIterations', 1_000_000);
Jiné názvy se tiše ignorují; neexistují limity délky řetězce, výstupu konzole ani doby běhu.
Limit smyčky se zatím nevynucuje všude. Smyčky běžící ve VM pro bajtový kód — ta pokrývá těla funkcí a programy nejvyšší úrovně, které kompilátor bajtového kódu přijme — se nikdy nepočítají, takže while (true) {} běží, dokud jej nezastaví PHP (problém #19). Nespoléhejte na tento limit při zastavování nedůvěryhodného kódu; použijte max_execution_time PHP nebo samostatný proces.
Při dosažení limitu může chybu zachytit JS try / catch (jako Error se zprávou „Maximum loop iterations exceeded“). Pokud ji nic nezachytí, dostane se do PHP jako Phasis\Exceptions\InternalError, který dědí z Phasis\Exceptions\RuntimeError, nikoli z JsThrowable.
Ve stroji jsou pevně nastaveny další dva limity, které nelze měnit za běhu: Phasis\Runtime\CallStack::maxDepth (4,096 JS rámců) a limit hloubky transpileovaných uzávěrů JsToPhp (8,192 PHP rámců). Při překročení obou dojde k chybě „Maximum call stack size exceeded“, která se také projeví v PHP jako InternalError. Jsou nastaveny výrazně nad úroveň, kterou může vyžadovat jakýkoli legitimní program v JS — jejich zvýšení vyžaduje změnu kódu, nejde o konfigurační volbu.
reset
public function reset(): void
Vymaže všechny globální proměnné definované uživatelem a obsah konzole a znovu vytvoří standardní knihovnu i smyčku událostí.
U stroje vytvořeného ve výchozím líném režimu reset() v současnosti odstraní všechny líně instalované vestavěné funkce: poté jsou Map, JSON, Math, console, fetch i ostatní nedefinované (issue #22). Než se to opraví, vytvořte místo toho nový Engine nebo jej vytvořte s eager: true, kde reset() funguje.
Smyčka událostí
Stroj obsahuje vlastní smyčku událostí JavaScript s setTimeout, setInterval, clearTimeout, clearInterval a queueMicrotask na globálním objektu. Mik úlohy (obslužné funkce Promise, queueMicrotask) se zpracují mezi každou makro úlohou; časovače se spouštějí podle termínu prostřednictvím fronty úloh daného realm.
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
eval() a evalAsModule() automaticky zavolají runEventLoop() po dokončení synchronního kódu. Skript, který na nejvyšší úrovni naplánuje setTimeout(cb, 5000), se tedy spustí cb ještě před návratem eval() — smyčka používá virtuální čas a posune hodiny k nejbližšímu nevyřízenému termínu, místo aby čekala PHP. Odpovídá sémantice node script.js.
Pro integraci v reálném čase (dlouho běžící servery, integrace UI, plánování podle skutečných hodin) provede tickEventLoop() jednu iteraci (zpracuje mik úlohy a spustí všechny časovače, jejichž termín nastal) a vrátí se. pendingTaskCount() uvádí počet nevyřízených časovačů; hostitelský program může řídit vlastní smyčku:
while ($engine->pendingTaskCount() > 0) {
$engine->tickEventLoop();
usleep(1000);
}
Nekonečně běžící setInterval má omezenou délku: runEventLoop() končí po 100,000 cyklech a vyvolá běžnou chybu \RuntimeException — chyba v kódu JS se tak projeví jako jasná chyba PHP, místo aby program zamrzl. Interval zůstane naplánovaný, takže každé další eval() na stejném stroji vyvolá stejnou chybu; poté stroj zahoďte. Nezachycené chyby ve zpětném volání časovače se předávají přes error_log() a smyčka pokračuje ve zpracování; chyba jednoho časovače program neukončí.
Háčky Fetch
Phasis má ve výchozím nastavení povolené fetch(), které jako přenosovou vrstvu využívá ext-curl od PHP. Tři háčky umožňují hostitelskému programu změnit přenosovou vrstvu, řídit požadavky nebo povolit ukládání cookies. Každý přijímá volatelnou funkci PHP nebo metodu objektu; smlouvu pro tvar hodnoty najdete v části Web APIs.
setFetchTransport
public function setFetchTransport(callable $transport): void
Nahradí výchozí CurlTransport. Volatelná funkce přijímá popis požadavku a volitelný JsObject AbortSignal a vrací popis odpovědi.
// 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(),
];
});
Běžný způsob použití při integraci: v testech nainstalujte náhradní přenosovou vrstvu a v produkci skutečný přenos přes curl.
setFetchPolicy
public function setFetchPolicy(callable $hook): void
Háček zásad pro kontrolu před odesláním požadavku. Přijímá JsObject Request; může vyvolat chybu a požadavek zamítnout, vrátit upravené Request a požadavek přepsat, nebo vrátit null či žádnou hodnotu a požadavek povolit.
// 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
Volitelné úložiště cookies. Přijímá libovolný objekt zpřístupňující get(string $url): string a set(string $url, string $header): void — na straně PHP nebo JS.
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());
Bez setCookieJar() fetch ignoruje příchozí Set-Cookie i odchozí hlavičky Cookie — ve výchozím nastavení se nepoužívají žádné cookies.
Výjimky
| Třída | Rozšiřuje | Kdy |
|---|---|---|
Phasis\Exceptions\JsThrowable |
RuntimeError |
explicitní JS throw, kterou nic nezachytilo |
Phasis\Exceptions\TypeError, ReferenceError, RangeError |
RuntimeError |
chyba vyvolaná samotným strojem (například null.x nebo nedefinovaná proměnná), kterou nic nezachytilo |
Phasis\Exceptions\InternalError |
RuntimeError |
byl překročen limit smyčky nebo hloubky volání |
Phasis\Exceptions\RuntimeError |
\RuntimeException |
základní třída výše uvedených tříd |
Phasis\Parser\ParseError |
Phasis\Exceptions\SyntaxError (\RuntimeException) |
zdrojový kód nelze parsovat |
\RuntimeException |
execFile() nemůže přečíst soubor; smyčka událostí narazila na limit 100,000 cyklů |
Chcete-li zachytit všechna selhání skriptu, zachyťte Phasis\Exceptions\RuntimeError a Phasis\Exceptions\SyntaxError nebo jednoduše \RuntimeException.
Výjimky vyvolané vašimi vlastními hostitelskými funkcemi PHP se chovají jinak: v současnosti procházejí přes JS try / catch a beze změny dorazí k volajícímu eval(), včetně původní třídy (issue #20). Viz Hostitelské funkce.
Životní cyklus
Engine se vytváří rychle (za studena několik ms) a standardní knihovna se instaluje líně. Ve většině případů integrace stačí jeden dlouho běžící stroj pro každý proces. Při použití více klienty dejte každému klientovi nebo požadavku vlastní stroj, aby mezi nimi nedocházelo k úniku globálních proměnných. Spolehlivým způsobem, jak začít s čistým prostředím, je vytvořit nový stroj (viz reset).
foreach ($requests as $req) {
$engine = new Engine();
$engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
$engine->execFile($req->scriptPath);
}