İçeriğe geç

API

Yürütme API tek bir sınıftan oluşur: Phasis\Engine. Diğer her şey (JsValue, JsFunction, JsObject, …) motorun içindedir ve yalnızca PHP↔JS arasında köprü kuran ana bilgisayar işlevlerine açılır. Diğer tek genel arayüz, kaynak kodunu çalıştırmadan ayrıştıran bağımsız ayrıştırıcıAPI'dır (Phasis\Parser\Parser ve Phasis\Ast sınıfları).

Oluşturma

PHP
use Phasis\Engine;

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

Yeni bir Engine, standart kitaplığın tamamını kullanıma sunar — Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error ve temel dil intrinsics bileşenleri hemen yüklenir, böylece spesifikasyon çalışır (Function.prototype olmadan sınıflar, Promise olmadan async/await, RegExp olmadan regex sabitleri kullanılamaz). Diğer her şey — Math, JSON, Map, Set, Date, TypedArray ve dizi arabellek ailesi, Atomics, Proxy, Reflect, console, Intl, weak-ref ailesi, Temporal, ShadowRealm, Web Platform Pack ve Fetch Pack — yer tutucu erişimci olarak kaydedilir ve ilk okumada oluşturulur. Genel değişkeni okumak şeffaf biçimde gerçekleşir: Map, typeof Map, 'Map' in globalThis, Object.keys(globalThis).includes('Map') hepsi çalışır ve ilk okumadan sonra yer tutucu, normal bir veri tanımlayıcısıyla değiştirilir.

Özellikle tembel yükleme öncesindeki davranış gerekiyorsa eager: true parametresini iletin — tüm yerleşik öğeler oluşturma sırasında kurulur ve tam tanımlayıcı yüzeyi iç gözlem için görünür olur. Uygunluk test düzeneği (bin/test262, bin/wpt), motorları hevesli modda oluşturur; böylece test262/WPT beklenen tanımlayıcı biçimlerini görür.

eval

PHP
public function eval(string $source): mixed

$source öğesini motorun global kapsamındaki klasik bir betik olarak ayrıştırıp çalıştırır, ardından olay döngüsünü boşalana kadar yürütür. Son değerlendirilen ifadenin PHP türüne dönüştürülmüş değerini döndürür (programın sonunda ifade yoksa null). Kurallar için Değer dönüştürme bölümüne bakın.

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 açımlanmaz: eval('Promise.resolve(5)') boş bir dizi döndürür. eval() döndüğünde olay döngüsü tamamlanmış olduğundan, sonuçlanan değeri bir globale kaydedip ikinci bir eval() ile okuyun:

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

Hiçbir şey tarafından yakalanmayan açık bir JS throw, mesajı "Name: message" olan ve $jsValue özelliğinde fırlatılan JsValue değerini tutan bir Phasis\Exceptions\JsThrowable olarak görünür. Motorun kendisinin oluşturduğu hatalar (örneğin null özelliğini okumak veya tanımlanmamış bir değişken kullanmak) bunun yerine Phasis\Exceptions\TypeError, ReferenceError veya RangeError olarak görünür. Bunların tümü Phasis\Exceptions\RuntimeError sınıfını genişletir; ayrıştırma hataları ise Phasis\Exceptions\SyntaxError sınıfını genişletir. İstisnalar bölümüne bakın.

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

$source öğesini bir ES modülü olarak değerlendirir: import / export, üst düzey await ve import.meta kullanılabilir; göreli belirteçler $path (mutlak dosya yolu) temel alınarak çözümlenir. Olay döngüsünü eval() ile aynı şekilde çalıştırır ve her zaman null döndürür; sonuçları globaller veya konsol arabelleği üzerinden okuyun.

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

Yükleyici yalnızca göreli ve mutlak yolları çözümler; Bilinen sınırlamalar bölümüne bakın.

execFile

PHP
public function execFile(string $path): mixed

Diskten dosya okur ve içeriğini eval() öğesine iletir; bu nedenle uzantısı ne olursa olsun klasik betik olarak çalışır. import / export içeren bir dosya SyntaxError hatası verir. Dosyanın gerçek yolu, dinamik import() belirteçleri için temel yol olur. Modül dosyaları için evalAsModule() kullanın.

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

Dosya okunamazsa PHP bir uyarı verir ve execFile() sıradan bir \RuntimeException fırlatır ("Cannot read file: …").

setGlobal

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

Sonraki tüm JS değerlendirmelerinde görülebilen bir global değişken bağlar. PHP değerleri JS türüne dönüştürülür:

PHP türü JS türü
null null
bool boolean
int / float number
string string (işlev adı olsa bile, örneğin 'strlen')
dizi (liste) Array (kopya)
dizi (eşlemeli dizi) Object (kopya)
Closure, çağrılabilir nesne, [$object, 'method'] function
diğer nesneler, üreteçler veya kaynaklar undefined

Diziler kopyalanır: JS üzerinde yapılan değişiklikler PHP dizisinde görünmez. Sıradan PHP nesneleri henüz iletilemez (issue #18); geçici çözümler için Paylaşılan nesneler bölümüne bakın.

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

JS içinde:

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

call

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

Global bir JS işlevini PHP bağımsız değişkenle çağırır (setGlobal için kullanılan kurallara göre dönüştürülür). Dönüş değeri yeniden PHP türüne dönüştürülür. Tanımsız bir adın çağrılması Phasis\Exceptions\ReferenceError hatası fırlatır; işlev olmayan bir globalin çağrılması Phasis\Exceptions\TypeError hatası fırlatır.

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

Konsol çıktısı

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

console.log, console.info, console.warn, console.error ve console.debug çıktı vermez. Her çağrı, motor üzerindeki bir arabelleğe biçimlendirilmiş tek bir satır ekler. getConsoleLines() arabellekteki satırları dizge listesi olarak döndürür, getConsoleOutput() bunları \n ile birleştirerek döndürür ve clearConsole() arabelleği boşaltır. Arabellek, eval() çağrıları arasında temizlenmez.

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

Çalışma zamanında ayarlanabilen bir kaynak sınırı belirler. Yalnızca bir tane vardır:

Sınır Varsayılan Açıklama
maxLoopIterations 100000 ağaç dolaşan yorumlayıcının çalıştırdığı her for / while / do…while döngü çalıştırmasındaki en yüksek yineleme sayısı
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Diğer adlar sessizce yok sayılır; dizge uzunluğu, konsol çıktısı veya gerçek zaman için sınır yoktur.

Döngü sınırı henüz her yerde uygulanmaz. Bayt kodu VM'sinde çalışan döngüler (işlev gövdelerini ve bayt kodu derleyicisinin kabul ettiği üst düzey programları kapsar) hiç sayılmaz; dolayısıyla while (true) {}, PHP durdurana kadar çalışır (issue #19). Güvenilmeyen kodu durduracağına güvenmeyin; PHP öğesinin max_execution_time özelliğini veya ayrı bir süreci kullanın.

Sınıra ulaşıldığında hata, bir JS try / catch tarafından yakalanabilir ("Maximum loop iterations exceeded" mesajını taşıyan bir Error olarak). Hiçbir şey yakalamazsa, PHP öğesine Phasis\Exceptions\InternalError olarak ulaşır; bu sınıf Phasis\Exceptions\RuntimeError sınıfını genişletir, JsThrowable sınıfını değil.

İki ek üst sınır motora sabitlenmiştir ve çalışma zamanında ayarlanamaz: Phasis\Runtime\CallStack::maxDepth (4,096 JS kare) ve JsToPhp dönüştürülmüş kapanış derinliği koruması (8,192 PHP kare). Her ikisi de aşıldığında "Maximum call stack size exceeded" hatasını fırlatır; bu da PHP öğesine InternalError olarak ulaşır. Her ikisi de meşru JS programlarının çok üzerinde ayarlanmıştır; artırmak bir yapılandırma seçeneği değil, kod değişikliğidir.

reset

PHP
public function reset(): void

Kullanıcı tanımlı tüm globalleri ve konsol arabelleğini temizler; standart kütüphaneyi ve olay döngüsünü yeniden oluşturur.

Varsayılan tembel modda oluşturulmuş bir motorda reset() şu anda tembel yüklenen tüm yerleşik öğeleri kaybeder: sonrasında Map, JSON, Math, console, fetch ve diğerleri tanımsız olur (issue #22). Bu düzeltilene kadar bunun yerine yeni bir Engine oluşturun veya eager: true ile oluşturun; bu durumda reset() çalışır.

Olay döngüsü

Motor, global nesnede setTimeout, setInterval, clearTimeout, clearInterval ve queueMicrotask bulunan bir JavaScript olay döngüsüyle gelir. Mikro görevler (Promise işleyicileri, queueMicrotask), her makro görev arasında tamamlanır; zamanlayıcılar, her realm'e ait görev kuyruğunda son tarih sırasına göre çalışır.

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

eval() ve evalAsModule(), eşzamanlı kod tamamlandıktan sonra runEventLoop() öğesini otomatik olarak çağırır. Bu nedenle, en üst düzeyde setTimeout(cb, 5000) planlayan bir betik, eval() dönmeden önce cb çalıştırır; döngü uyumak PHP yerine saatini sonraki bekleyen son tarihe atlayarak sanal zaman kullanır. node script.js semantiğiyle eşleşir.

Gerçek zamanlı gömme (uzun ömürlü sunucular, UI entegrasyonu, gerçek saate göre zamanlama) için tickEventLoop() tek bir yineleme çalıştırır (mikro görevleri tamamlar, zamanı gelen zamanlayıcıları çalıştırır) ve döner. pendingTaskCount() bekleyen zamanlayıcıları bildirir; gömen uygulama kendi döngüsünü çalıştırabilir:

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

Kontrolden çıkan setInterval sınırlandırılır: runEventLoop() en fazla 100,000 döngü çalıştırır ve sıradan bir \RuntimeException fırlatır; JS kodunun hatası takılma yerine açık bir PHP hatası olarak görünür. Aralık zamanlanmış durumda kalır; bu nedenle aynı motor üzerindeki sonraki her eval() aynı hatayı fırlatır; sonrasında motoru atın. Zamanlayıcı geri çağrısındaki yakalanmamış hatalar error_log() üzerinden iletilir ve döngü görevleri tamamlamaya devam eder; hatalı bir zamanlayıcı programı durdurmaz.

Fetch kancaları

Phasis, altta yatan aktarım katmanı olarak PHP öğesinin ext-curl özelliğiyle varsayılan olarak etkin fetch() sunar. Üç kanca, gömen uygulamanın aktarım katmanını değiştirmesine, istekleri denetlemesine veya çerez depolamasını etkinleştirmesine olanak tanır. Üçü de PHP çağrılabilirini veya nesne üzerindeki bir yöntemi kabul eder; değer biçimi sözleşmesi için Web API'leri bölümüne bakın.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Varsayılan CurlTransport'ı değiştirir. Çağrılabilir öğe bir istek tanımlayıcısı ve isteğe bağlı AbortSignal JsObject alır, ardından bir yanıt tanımlayıcısı döndürür.

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

Yaygın bir gömme örüntüsü: testlerde taklit aktarım katmanı, üretimde gerçek curl aktarım katmanı kurmak.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

İstek öncesi politika kancası. Request JsObject'i alır; isteği reddetmek için hata fırlatabilir, yeniden yazmak için değiştirilmiş bir Request döndürebilir veya izin vermek için null / değer döndürmeyebilir.

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

İsteğe bağlı çerez deposu. get(string $url): string ve set(string $url, string $header): void öğelerini sunan herhangi bir nesneyi kabul eder — PHP tarafında veya JS tarafında.

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

setCookieJar() olmadan fetch, gelen Set-Cookie ve giden Cookie başlıklarını yok sayar; varsayılan davranış çerez kullanmamaktır.

İstisnalar

Sınıf Genişlettiği Durum
Phasis\Exceptions\JsThrowable RuntimeError Hiçbir şeyin yakalamadığı açık bir JS throw
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError Motorun kendisinin oluşturduğu (örneğin null.x veya tanımsız değişken) ve hiçbir şeyin yakalamadığı hata
Phasis\Exceptions\InternalError RuntimeError Döngü sınırına veya çağrı yığını üst sınırına ulaşılması
Phasis\Exceptions\RuntimeError \RuntimeException Yukarıdakilerin temel sınıfı
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) Kaynak kodunun ayrıştırılamaması
\RuntimeException execFile() dosyayı okuyamıyor; olay döngüsü 100,000 döngülük sınırına ulaştı

Bir betikteki tüm hataları yakalamak için Phasis\Exceptions\RuntimeError ve Phasis\Exceptions\SyntaxError hatalarını yakalayın veya doğrudan \RuntimeException kullanın.

Kendi PHP ana bilgisayar işlevlerinizin fırlattığı istisnalar farklıdır: şu anda JS try / catch üzerinden olduğu gibi geçer ve özgün sınıflarıyla eval() çağrısına ulaşır (issue #20). Ana bilgisayar işlevleri bölümüne bakın.

Yaşam döngüsü

Engine oluşturmak kolaydır (soğuk başlatmada ~birkaç ms) ve standart kütüphane tembel olarak yüklenir. Çoğu gömme senaryosunda süreç başına tek ve uzun ömürlü bir motor yeterlidir. Çok kiracılı kullanımda, globallerin kiracılar arasında sızmaması için her kiracıya veya isteğe ayrı bir motor verin. Bugün temiz bir başlangıç yapmanın güvenilir yolu yeni bir motor oluşturmaktır (reset bölümüne bakın).

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