Przejdź do treści

API

Interfejs wykonawczy API to jedna klasa: Phasis\Engine. Wszystko inne (JsValue, JsFunction, JsObject, …) jest wewnętrzne dla silnika i udostępniane wyłącznie funkcjom hosta łączącym PHP↔JS. Jedynym innym publicznym interfejsem jest samodzielny parser parser API (klasy Phasis\Parser\Parser i Phasis\Ast), który analizuje kod źródłowy bez jego wykonywania.

Tworzenie

PHP
use Phasis\Engine;

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

Nowy Engine udostępnia pełną bibliotekę standardową — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error oraz podstawowe elementy języka są instalowane od razu, aby specyfikacja działała (klasy wymagają Function.prototype, async/await — Promise, a literały wyrażeń regularnych — RegExp). Wszystkie pozostałe elementy — Math, JSON, Map, Set, Date, TypedArray i rodzina array-buffer, Atomics, Proxy, Reflect, console, Intl, rodzina weak-ref, Temporal, ShadowRealm, Web Platform Pack oraz Fetch Pack — są rejestrowane jako akcesory zastępcze i tworzone przy pierwszym odczycie. Odczyt zmiennej globalnej jest przezroczysty: działają Map, typeof Map, 'Map' in globalThis, Object.keys(globalThis).includes('Map'), a po pierwszym odczycie akcesor zastępczy zostaje zastąpiony zwykłym deskryptorem danych.

Przekaż eager: true, jeśli potrzebujesz zachowania sprzed wprowadzenia leniwego ładowania — wszystkich funkcji wbudowanych instalowanych podczas tworzenia, z pełnym zestawem deskryptorów widocznym podczas introspekcji. Harness zgodności (bin/test262, bin/wpt) tworzy silniki w trybie eager, aby test262/WPT widziały oczekiwane kształty deskryptorów.

eval

PHP
public function eval(string $source): mixed

Analizuje i wykonuje $source jako klasyczny skrypt w globalnym zakresie silnika, a następnie uruchamia pętlę zdarzeń, aż zostanie opróżniona. Zwraca wartość ostatniego obliczonego wyrażenia przekonwertowaną na PHP (lub null, jeśli program nie kończył się wyrażeniem). Zasady opisano w sekcji Konwersja wartości.

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)

Promise nie jest rozpakowywany: eval('Promise.resolve(5)') zwraca pustą tablicę. Ponieważ pętla zdarzeń zostaje opróżniona, zanim eval() zwróci wynik, zapisz rozstrzygniętą wartość w zmiennej globalnej i odczytaj ją drugim wywołaniem eval():

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

Jawne JS throw, którego nic nie przechwytuje, pojawia się jako Phasis\Exceptions\JsThrowable; jego komunikat to "Name: message", a właściwość $jsValue przechowuje rzucone JsValue. Błędy zgłaszane przez sam silnik, takie jak odczyt właściwości null lub niezdefiniowanej zmiennej, pojawiają się zamiast tego jako Phasis\Exceptions\TypeError, ReferenceError lub RangeError. Wszystkie te klasy rozszerzają Phasis\Exceptions\RuntimeError; błędy parsowania rozszerzają Phasis\Exceptions\SyntaxError. Zobacz sekcję Wyjątki.

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

Oblicza $source jako moduł ES: dostępne są import / export, najwyższopoziomowe await i import.meta, a specyfikatory względne są rozwiązywane względem $path (bezwzględnej ścieżki pliku). Uruchamia pętlę zdarzeń tak jak eval() i zawsze zwraca null; wyniki odczytuj przez zmienne globalne lub bufor konsoli.

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

Loader rozwiązuje wyłącznie ścieżki względne i bezwzględne; zobacz sekcję Znane ograniczenia.

execFile

PHP
public function execFile(string $path): mixed

Odczytuje plik z dysku i przekazuje jego zawartość do eval(), więc plik jest uruchamiany jako klasyczny skrypt niezależnie od rozszerzenia; plik z import / export powoduje błąd SyntaxError. Rzeczywista ścieżka pliku staje się bazą dla dynamicznych specyfikatorów import(). W przypadku plików modułów użyj evalAsModule().

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

Jeśli nie można odczytać pliku, PHP wyświetla ostrzeżenie, a execFile() zgłasza zwykły \RuntimeException ("Cannot read file: …").

setGlobal

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

Wiąże zmienną globalną widoczną we wszystkich kolejnych wywołaniach JS. Wartości PHP są konwertowane na JS:

Typ PHP Typ JS
null null
bool boolean
int / float number
string string (nawet gdy oznacza funkcję, taką jak 'strlen')
array (lista) Array (kopia)
array (assoc) Object (kopia)
Closure, obiekt wywoływalny, [$object, 'method'] function
każdy inny obiekt, generator lub zasób undefined

Tablice są kopiowane: zmiany wprowadzane w nich przez JS nie są widoczne w tablicy PHP. Zwykłych obiektów PHP nie można jeszcze przekazywać (issue #18); obejścia opisano w sekcji Obiekty współdzielone.

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

Wewnątrz JS:

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

call

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

Wywołuje globalną funkcję JS z argumentami PHP (konwertowanymi tak jak w setGlobal). Wartość zwracana jest konwertowana z powrotem na PHP. Wywołanie niezdefiniowanej nazwy zgłasza Phasis\Exceptions\ReferenceError; wywołanie zmiennej globalnej, która nie jest funkcją, zgłasza Phasis\Exceptions\TypeError.

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

Wyjście konsoli

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

console.log, console.info, console.warn, console.error i console.debug niczego nie wyświetlają. Każde wywołanie dopisuje jedną sformatowaną linię do bufora silnika. getConsoleLines() zwraca buforowane linie jako listę ciągów, getConsoleOutput() zwraca je połączone za pomocą \n, a clearConsole() opróżnia bufor. Bufor nie jest czyszczony między wywołaniami 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

Ustawia limit zasobów regulowany w czasie działania. Jest dokładnie jeden:

Limit Domyślnie Opis
maxLoopIterations 100000 maksymalna liczba iteracji na przebieg pętli for / while / do…while wykonywanej przez interpreter przechodzący po drzewie
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Każda inna nazwa jest po cichu ignorowana; nie ma limitów długości ciągów, wyjścia konsoli ani czasu rzeczywistego.

Limit pętli nie jest jeszcze egzekwowany wszędzie. Pętle wykonywane na maszynie wirtualnej kodu bajtowego, obejmującej treści funkcji i programy najwyższego poziomu akceptowane przez kompilator kodu bajtowego, nigdy nie są zliczane, więc while (true) {} działa, dopóki nie zatrzyma go PHP (issue #19). Nie polegaj na tym mechanizmie, aby zatrzymywać niezaufany kod; użyj PHP z max_execution_time lub osobnego procesu.

Po osiągnięciu limitu błąd można przechwycić za pomocą JS try / catch (jako Error z komunikatem "Maximum loop iterations exceeded"). Jeśli nic go nie przechwyci, trafia do PHP jako Phasis\Exceptions\InternalError, która rozszerza Phasis\Exceptions\RuntimeError, a nie JsThrowable.

Dwa dodatkowe limity są na stałe wbudowane w silnik i nie można ich regulować w czasie działania: Phasis\Runtime\CallStack::maxDepth (ramki 4,096 JS) oraz limit głębokości domknięć transpilowanych przez JsToPhp (ramki 8,192 PHP). Po przekroczeniu obu zgłaszany jest błąd "Maximum call stack size exceeded", który również trafia do PHP jako InternalError. Limity są znacznie wyższe niż potrzeba jakiemukolwiek prawidłowemu programowi JS — ich podniesienie wymaga zmiany kodu, a nie ustawienia konfiguracji.

reset

PHP
public function reset(): void

Czyści wszystkie zmienne globalne zdefiniowane przez użytkownika i bufor konsoli oraz odbudowuje bibliotekę standardową i pętlę zdarzeń.

W silniku utworzonym w domyślnym trybie lazy funkcja reset() obecnie usuwa wszystkie leniwie instalowane funkcje wbudowane: później Map, JSON, Math, console, fetch i pozostałe są niezdefiniowane (issue #22). Dopóki nie zostanie to naprawione, utwórz nowy Engine albo utwórz go z eager: true, gdzie działa reset().

Pętla zdarzeń

Silnik zawiera własną pętlę zdarzeń JavaScript z setTimeout, setInterval, clearTimeout, clearInterval i queueMicrotask na obiekcie globalnym. Mikro-zadania (procedury obsługi Promise, queueMicrotask) są realizowane między każdym makrozadaniem; timery uruchamiają się według terminów za pośrednictwem kolejki zadań przypisanej do danej domeny.

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

eval() i evalAsModule() automatycznie wywołują runEventLoop() po zakończeniu kodu synchronicznego. Skrypt, który na najwyższym poziomie zaplanuje setTimeout(cb, 5000), wykona więc cb przed zwróceniem wyniku przez eval() — pętla używa czasu wirtualnego i przestawia zegar na kolejny oczekujący termin zamiast spać przez PHP. Zgodne z semantyką node script.js.

W przypadku osadzania w czasie rzeczywistym (serwery działające długotrwale, integracja UI, harmonogramowanie według rzeczywistego zegara) tickEventLoop() wykonuje jedną iterację (opróżnia mikro-zadania, uruchamia należne timery) i zwraca wynik. pendingTaskCount() informuje o oczekujących timerach; kod osadzający może obsługiwać własną pętlę:

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

Niekontrolowana pętla setInterval ma ograniczenie: runEventLoop() zatrzymuje się po 100,000 cyklach i zgłasza zwykły \RuntimeException — wina kodu JS jest sygnalizowana czytelnym błędem PHP, a nie zawieszeniem. Interwał pozostaje zaplanowany, więc każde późniejsze wywołanie eval() na tym samym silniku zgłosi ten sam błąd; później odrzuć silnik. Nieprzechwycone błędy w wywołaniu zwrotnym timera są emitowane przez error_log(), a pętla kontynuuje pracę; pojedynczy wadliwy timer nie zatrzymuje programu.

Haki Fetch

Phasis domyślnie dostarcza włączoną funkcję fetch(), używającą ext-curl z PHP jako transportu bazowego. Trzy haki pozwalają zastąpić transport, filtrować żądania lub włączyć przechowywanie ciasteczek. Każdy z nich akceptuje wywoływalny PHP albo metodę obiektu; opis dozwolonych kształtów wartości znajduje się w sekcji Interfejsy Web.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Zastępuje domyślny CurlTransport. Wywoływalny element otrzymuje deskryptor żądania i opcjonalny AbortSignal JsObject; zwraca deskryptor odpowiedzi.

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

Częsty wzorzec osadzania: instalacja mockowanego transportu w testach i prawdziwego transportu curl na produkcji.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Hak zasad przed wysłaniem żądania. Otrzymuje Request JsObject; może zgłosić wyjątek, aby odmówić, zwrócić zmodyfikowany Request, aby przepisać żądanie, lub zwrócić null/brak wartości, aby zezwolić.

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

Opcjonalny magazyn ciasteczek. Akceptuje dowolny obiekt udostępniający get(string $url): string i set(string $url, string $header): void — po stronie PHP lub JS.

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

Bez setCookieJar() fetch ignoruje zarówno przychodzące, jak i wychodzące nagłówki Set-Cookie i Cookie — domyślnie nie ma ciasteczek.

Wyjątki

Klasa Rozszerza Kiedy
Phasis\Exceptions\JsThrowable RuntimeError jawne JS throw, którego nic nie przechwyciło
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError błąd zgłoszony przez silnik (na przykład null.x lub niezdefiniowana zmienna), którego nic nie przechwyciło
Phasis\Exceptions\InternalError RuntimeError przekroczono limit pętli lub limit stosu wywołań
Phasis\Exceptions\RuntimeError \RuntimeException klasa bazowa powyższych
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) kod źródłowy nie daje się sparsować
\RuntimeException execFile() nie może odczytać pliku; pętla zdarzeń osiągnęła limit 100,000 cykli

Aby przechwycić każdy błąd skryptu, przechwytuj Phasis\Exceptions\RuntimeError i Phasis\Exceptions\SyntaxError albo po prostu \RuntimeException.

Wyjątki zgłaszane przez własne funkcje hosta PHP są inne: obecnie przechodzą przez JS try / catch i docierają bez zmian do wywołującego eval() wraz z pierwotną klasą (issue #20). Zobacz sekcję Funkcje hosta.

Cykl życia

Tworzenie Engine jest tanie (kilka ms przy zimnym starcie), a biblioteka standardowa jest instalowana leniwie. W większości przypadków osadzania wystarczy jeden długotrwale działający silnik na proces. Przy użyciu wielu dzierżawców utwórz osobny silnik dla każdego z nich lub dla każdego żądania, aby zmienne globalne nie wyciekały między nimi. Obecnie niezawodny czysty stan zapewnia utworzenie nowego silnika (zobacz reset).

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