Lewati ke konten

API

API eksekusi terdiri dari satu kelas: Phasis\Engine. Semua yang lain (JsValue, JsFunction, JsObject, …) bersifat internal bagi engine dan hanya diekspos ke fungsi host yang menjembatani PHP↔JS. Satu-satunya permukaan publik lainnya adalah parser mandiri API (kelas Phasis\Parser\Parser dan Phasis\Ast), yang mengurai sumber tanpa mengeksekusinya.

Konstruksi

PHP
use Phasis\Engine;

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

Engine baru mengekspos seluruh pustaka standar — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error serta intrinsik bahasa inti yang dipasang secara langsung agar spesifikasi berfungsi (tanpa Function.prototype Anda tidak dapat memiliki kelas, tanpa Promise tidak ada async/await, dan tanpa RegExp tidak ada literal regex). Semua yang lain — Math, JSON, Map, Set, Date, TypedArray dan keluarga array-buffer, Atomics, Proxy, Reflect, console, Intl, keluarga weak-ref, Temporal, ShadowRealm, Web Platform Pack, dan Fetch Pack — didaftarkan sebagai accessor placeholder dan diwujudkan saat pertama kali dibaca. Pembacaan global berlangsung transparan: Map, typeof Map, 'Map' in globalThis, Object.keys(globalThis).includes('Map') semuanya berfungsi, dan setelah pembacaan pertama placeholder diganti dengan deskriptor data biasa.

Teruskan eager: true jika Anda benar-benar membutuhkan perilaku sebelum lazy loading — setiap built-in dipasang saat konstruksi dengan seluruh permukaan deskriptor terlihat saat introspeksi. Harness kesesuaian (bin/test262, bin/wpt) membuat engine dalam mode eager agar test262/WPT melihat bentuk deskriptor yang diharapkan.

eval

PHP
public function eval(string $source): mixed

Mengurai dan mengeksekusi $source sebagai skrip klasik dalam cakupan global engine, lalu menjalankan event loop hingga kosong. Mengembalikan nilai ekspresi terakhir yang dievaluasi, dikonversi ke PHP (atau null jika program tidak memiliki ekspresi penutup). Lihat Konversi nilai untuk aturannya.

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 tidak di-unpack: eval('Promise.resolve(5)') mengembalikan array kosong. Karena event loop sudah selesai berjalan saat eval() mengembalikan hasil, simpan nilai yang telah diselesaikan dalam global dan baca dengan eval() kedua:

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

JS throw eksplisit yang tidak ditangkap akan muncul sebagai Phasis\Exceptions\JsThrowable, dengan pesan "Name: message" dan properti $jsValue yang menyimpan JsValue yang dilempar. Error yang dihasilkan engine sendiri, seperti membaca properti dari null atau variabel yang tidak didefinisikan, muncul sebagai Phasis\Exceptions\TypeError, ReferenceError, atau RangeError. Semua ini memperluas Phasis\Exceptions\RuntimeError; error penguraian memperluas Phasis\Exceptions\SyntaxError. Lihat Pengecualian.

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

Mengevaluasi $source sebagai modul ES: import / export, await tingkat teratas dan import.meta tersedia, sementara specifier relatif diselesaikan terhadap $path (jalur file absolut). Menjalankan event loop seperti eval() dan selalu mengembalikan null; baca hasil melalui global atau buffer console.

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

Pemuat hanya menyelesaikan jalur relatif dan absolut; lihat Batasan yang diketahui.

execFile

PHP
public function execFile(string $path): mixed

Membaca file dari disk dan meneruskan isinya ke eval(), sehingga file dijalankan sebagai skrip klasik apa pun ekstensinya; file dengan import / export gagal dengan SyntaxError. Jalur asli file menjadi basis bagi specifier import() dinamis. Gunakan evalAsModule() untuk file modul.

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

Jika file tidak dapat dibaca, PHP mengeluarkan peringatan dan execFile() melempar \RuntimeException biasa ("Cannot read file: …").

setGlobal

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

Mengikat variabel global yang terlihat dalam semua evaluasi JS berikutnya. Nilai PHP dikonversi menjadi JS:

Tipe PHP Tipe JS
null null
bool boolean
int / float number
string string (bahkan jika namanya merujuk ke fungsi, seperti 'strlen')
array (daftar) Array (salinan)
array (asosiatif) Object (salinan)
Closure, objek yang dapat dipanggil, [$object, 'method'] function
objek, generator, atau resource lainnya undefined

Array disalin: perubahan yang dibuat JS pada array tersebut tidak terlihat di array PHP. Objek PHP biasa belum dapat diteruskan (issue #18); lihat Objek bersama untuk solusinya.

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

Di dalam JS:

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

call

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

Memanggil fungsi global JS dengan argumen PHP (dikonversi seperti pada setGlobal). Nilai yang dikembalikan dikonversi kembali ke PHP. Memanggil nama yang tidak didefinisikan akan melempar Phasis\Exceptions\ReferenceError; memanggil global yang bukan fungsi akan melempar Phasis\Exceptions\TypeError.

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

Keluaran console

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

console.log, console.info, console.warn, console.error, dan console.debug tidak mencetak apa pun. Setiap pemanggilan menambahkan satu baris berformat ke buffer pada engine. getConsoleLines() mengembalikan baris dalam buffer sebagai daftar string, getConsoleOutput() mengembalikannya dengan \n sebagai pemisah, dan clearConsole() mengosongkan buffer. Buffer tidak dihapus di antara pemanggilan 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

Menetapkan batas sumber daya yang dapat diubah saat runtime. Hanya ada satu:

Batas Default Deskripsi
maxLoopIterations 100000 jumlah maksimum iterasi per pelaksanaan loop for / while / do…while oleh interpreter tree-walking
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Nama lain apa pun diabaikan tanpa pemberitahuan; tidak ada batas untuk panjang string, keluaran console, atau waktu dinding.

Batas loop belum diterapkan di semua tempat. Loop yang berjalan pada VM bytecode, yang mencakup isi fungsi dan program tingkat teratas yang diterima compiler bytecode, tidak pernah dihitung, sehingga while (true) {} berjalan sampai PHP menghentikannya (issue #19). Jangan mengandalkannya untuk menghentikan kode yang tidak tepercaya; gunakan PHP milik max_execution_time atau proses terpisah.

Saat batas tercapai, error dapat ditangkap oleh JS try / catch (sebagai Error dengan pesan "Maximum loop iterations exceeded"). Jika tidak ada yang menangkapnya, error sampai ke PHP sebagai Phasis\Exceptions\InternalError, yang memperluas Phasis\Exceptions\RuntimeError, bukan JsThrowable.

Dua batas tambahan tertanam dalam engine dan tidak dapat diubah saat runtime: Phasis\Runtime\CallStack::maxDepth (4,096 frame JS) dan pengaman kedalaman closure transpiled JsToPhp (8,192 frame PHP). Keduanya melempar "Maximum call stack size exceeded" saat terlampaui, dan error itu juga sampai ke PHP sebagai InternalError. Batas ini jauh di atas kebutuhan program JS yang sah — menaikkannya memerlukan perubahan kode, bukan pengaturan konfigurasi.

reset

PHP
public function reset(): void

Menghapus semua global yang ditentukan pengguna dan buffer console, serta membangun ulang pustaka standar dan event loop.

Pada engine yang dibuat dalam mode lazy default, reset() saat ini menghilangkan semua built-in yang dipasang secara lazy: setelahnya Map, JSON, Math, console, fetch, dan lainnya tidak terdefinisi (issue #22). Sampai masalah ini diperbaiki, buat Engine baru atau buat dengan eager: true, yang membuat reset() berfungsi.

Event loop

Engine menyertakan event loop JavaScript dengan setTimeout, setInterval, clearTimeout, clearInterval, dan queueMicrotask pada objek global. Microtask (penangan Promise, queueMicrotask) dikuras di antara setiap macrotask; timer berjalan berdasarkan urutan tenggat melalui antrean tugas per-realm.

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

eval() dan evalAsModule() memanggil runEventLoop() secara otomatis setelah kode sinkron selesai. Jadi, skrip yang menjadwalkan setTimeout(cb, 5000) di tingkat teratas berjalan cb sebelum eval() kembali — loop menggunakan waktu virtual, memajukan jamnya ke tenggat berikutnya yang tertunda alih-alih menunggu PHP. Sesuai dengan semantik node script.js.

Untuk penyematan waktu nyata (server berumur panjang, integrasi UI, penjadwalan berdasarkan jam nyata), tickEventLoop() menjalankan satu iterasi (menguras microtask, menjalankan timer yang waktunya tiba) lalu kembali. pendingTaskCount() melaporkan timer yang masih tertunda; embedder dapat menjalankan pump-nya sendiri:

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

setInterval yang berjalan tanpa henti dibatasi: runEventLoop() membatasi hingga 100,000 siklus lalu melempar \RuntimeException biasa — kesalahan pada kode JS muncul sebagai error PHP yang jelas, bukan membuat proses macet. Interval tetap terjadwal, jadi setiap eval() berikutnya pada engine yang sama akan melempar error yang sama; buang engine tersebut setelahnya. Error yang tidak tertangkap dalam callback timer dikeluarkan melalui error_log() dan loop terus menguras antrean; satu timer yang bermasalah tidak menghentikan program.

Hook fetch

Phasis menyertakan fetch() yang aktif secara default, dengan ext-curl milik PHP sebagai transport yang mendasarinya. Tiga hook memungkinkan embedder mengganti transport, membatasi permintaan, atau mengaktifkan penyimpanan cookie. Ketiganya menerima callable PHP atau metode pada objek; lihat API Web untuk kontrak bentuk nilai.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Ganti CurlTransport default. Callable menerima deskriptor permintaan dan JsObject AbortSignal opsional; mengembalikan deskriptor respons.

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

Pola penyematan yang umum: pasang transport tiruan dalam pengujian, lalu transport curl sungguhan di produksi.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Hook kebijakan pra-penerbangan. Menerima JsObject Request; dapat melempar error untuk menolak, mengembalikan Request yang diubah untuk menulis ulang, atau mengembalikan null/tanpa nilai untuk mengizinkan.

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

Penyimpanan cookie opsional. Menerima objek apa pun yang mengekspos get(string $url): string dan set(string $url, string $header): void — di sisi PHP atau sisi 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());

Tanpa setCookieJar(), fetch mengabaikan header Set-Cookie masuk dan Cookie keluar — default-nya tanpa cookie.

Exception

Class Extends Kapan
Phasis\Exceptions\JsThrowable RuntimeError JS throw eksplisit yang tidak ditangkap apa pun
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError error yang dibuat engine sendiri (misalnya null.x atau variabel yang tidak terdefinisi) dan tidak ditangkap apa pun
Phasis\Exceptions\InternalError RuntimeError batas loop atau batas tumpukan pemanggilan terlampaui
Phasis\Exceptions\RuntimeError \RuntimeException kelas dasar untuk yang di atas
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) sumber tidak dapat diuraikan
\RuntimeException execFile() tidak dapat membaca file; event loop mencapai batas 100,000 siklusnya

Untuk menangkap setiap kegagalan skrip, tangkap Phasis\Exceptions\RuntimeError dan Phasis\Exceptions\SyntaxError, atau cukup tangkap \RuntimeException.

Exception yang dilempar oleh fungsi host PHP Anda sendiri berbeda: saat ini exception tersebut melewati JS try / catch dan sampai ke pemanggil eval() tanpa perubahan, dengan kelas aslinya (masalah #20). Lihat Fungsi host.

Siklus hidup

Engine murah untuk dibuat (~beberapa ms saat cold start) dan pustaka standarnya dipasang secara lazy. Untuk sebagian besar skenario penyematan, satu engine berumur panjang per proses sudah memadai. Untuk penggunaan multi-tenant, berikan setiap tenant atau permintaan engine sendiri agar global tidak bocor di antara mereka. Membuat engine baru adalah cara andal untuk memulai dengan kondisi bersih saat ini (lihat reset).

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