Μετάβαση στο περιεχόμενο

API

Η επιφάνεια εκτέλεσης API αποτελείται από μία κλάση: Phasis\Engine. Όλα τα υπόλοιπα (JsValue, JsFunction, JsObject, …) είναι εσωτερικά της μηχανής και εκτίθενται μόνο σε συναρτήσεις υποδοχής που γεφυρώνουν τα PHP↔JS. Η μόνη άλλη δημόσια επιφάνεια είναι ο αυτόνομος parser API (οι κλάσεις Phasis\Parser\Parser και Phasis\Ast), που αναλύει τον πηγαίο κώδικα χωρίς να τον εκτελεί.

Δημιουργία

PHP
use Phasis\Engine;

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

Ένα νέο Engine εκθέτει την πλήρη τυπική βιβλιοθήκη — τα Object, Array, String, Number, Symbol, BigInt, Promise, RegExp, Error και τα βασικά ενσωματωμένα στοιχεία της γλώσσας εγκαθίστανται άμεσα, ώστε να λειτουργούν οι προδιαγραφές (δεν υπάρχουν κλάσεις χωρίς Function.prototype, async/await χωρίς Promise, ή λεκτικά regex χωρίς RegExp). Όλα τα υπόλοιπα — τα Math, JSON, Map, Set, Date, TypedArray και η οικογένεια array-buffer, τα Atomics, Proxy, Reflect, console, Intl, η οικογένεια weak-ref, τα Temporal, ShadowRealm, το Web Platform Pack και το Fetch Pack — καταχωρίζονται ως προσπελάσεις-υποκατάστατα και δημιουργούνται κατά την πρώτη ανάγνωση. Η ανάγνωση του καθολικού στοιχείου είναι διαφανής: τα Map, typeof Map, 'Map' in globalThis και Object.keys(globalThis).includes('Map') λειτουργούν, και μετά την πρώτη ανάγνωση το υποκατάστατο αντικαθίσταται από έναν κανονικό περιγραφέα δεδομένων.

Περάστε το eager: true αν χρειάζεστε συγκεκριμένα την προ-τεμπέλικη συμπεριφορά — κάθε ενσωματωμένο στοιχείο εγκαθίσταται κατά τη δημιουργία, με όλη την επιφάνεια περιγραφέα ορατή στην ενδοσκόπηση. Η υποδομή δοκιμών συμμόρφωσης (bin/test262, bin/wpt) δημιουργεί μηχανές σε άμεση λειτουργία, ώστε τα test262/WPT να βλέπουν τις μορφές περιγραφέα που αναμένουν.

αξιολόγηση

PHP
public function eval(string $source): mixed

Αναλύει και εκτελεί το $source ως κλασικό script στο καθολικό πεδίο της μηχανής και, στη συνέχεια, εκτελεί τον βρόχο συμβάντων μέχρι να αδειάσει. Επιστρέφει την τιμή της τελευταίας αξιολογημένης έκφρασης, μετατρεπόμενη σε PHP (ή null αν το πρόγραμμα δεν είχε τελική έκφραση). Δείτε τους κανόνες στη Μετατροπή τιμών.

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 δεν αποσυσκευάζεται: το eval('Promise.resolve(5)') επιστρέφει κενό πίνακα. Επειδή ο βρόχος συμβάντων έχει αδειάσει μέχρι να επιστρέψει το eval(), αποθηκεύστε την τιμή που έχει επιλυθεί σε ένα καθολικό στοιχείο και διαβάστε την με δεύτερο eval():

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

Ένα ρητό JS throw που δεν συλλαμβάνεται από τίποτα εμφανίζεται ως Phasis\Exceptions\JsThrowable, με μήνυμα "Name: message" και ιδιότητα $jsValue που περιέχει το JsValue που προκάλεσε την εξαίρεση. Τα σφάλματα που εγείρει η ίδια η μηχανή, όπως η ανάγνωση ιδιότητας του null ή μιας μη ορισμένης μεταβλητής, εμφανίζονται ως Phasis\Exceptions\TypeError, ReferenceError ή RangeError. Όλα αυτά επεκτείνουν το Phasis\Exceptions\RuntimeError· τα σφάλματα ανάλυσης επεκτείνουν το Phasis\Exceptions\SyntaxError. Δείτε τις Εξαιρέσεις.

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 ως ES module: είναι διαθέσιμα τα import / export και τα await και import.meta ανώτατου επιπέδου, ενώ οι σχετικοί προσδιοριστές επιλύονται με βάση το $path (απόλυτη διαδρομή αρχείου). Εκτελεί τον βρόχο συμβάντων όπως το eval() και επιστρέφει πάντα το null· διαβάστε τα αποτελέσματα μέσω καθολικών στοιχείων ή του buffer κονσόλας.

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

Ο φορτωτής επιλύει μόνο σχετικές και απόλυτες διαδρομές· δείτε τους Γνωστούς περιορισμούς.

execFile

PHP
public function execFile(string $path): mixed

Διαβάζει ένα αρχείο από τον δίσκο και περνά τα περιεχόμενά του στο eval(), ώστε να εκτελεστεί ως κλασικό script ανεξάρτητα από την επέκτασή του· ένα αρχείο με import / export αποτυγχάνει με SyntaxError. Η πραγματική διαδρομή του αρχείου γίνεται η βάση για δυναμικούς προσδιοριστές import(). Για αρχεία module χρησιμοποιήστε το evalAsModule().

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

Αν δεν είναι δυνατή η ανάγνωση του αρχείου, το PHP εμφανίζει προειδοποίηση και το execFile() προκαλεί απλό \RuntimeException ("Cannot read file: …").

setGlobal

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

Δένει μια καθολική μεταβλητή, ορατή σε όλες τις επόμενες αξιολογήσεις του JS. Οι τιμές PHP μετατρέπονται σε JS:

Τύπος PHP Τύπος JS
null null
bool boolean
int / float number
string string (ακόμη κι όταν είναι όνομα συνάρτησης, όπως το 'strlen')
array (λίστα) Array (αντίγραφο)
array (συσχετιστικός) Object (αντίγραφο)
Closure, αντικείμενο με δυνατότητα κλήσης, [$object, 'method'] function
οποιοδήποτε άλλο αντικείμενο, generator ή πόρος undefined

Οι πίνακες αντιγράφονται: οι αλλαγές που κάνει το JS σε αυτούς δεν είναι ορατές στον πίνακα PHP. Δεν είναι ακόμη δυνατή η μεταβίβαση απλών αντικειμένων PHP (issue #18)· δείτε τις Κοινόχρηστες οντότητες για εναλλακτικές λύσεις.

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

Μέσα στο JS:

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

κλήση

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

Καλεί μια καθολική συνάρτηση JS με ορίσματα PHP (που μετατρέπονται όπως για το setGlobal). Η τιμή επιστροφής μετατρέπεται ξανά σε PHP. Η κλήση ονόματος που δεν έχει οριστεί προκαλεί Phasis\Exceptions\ReferenceError· η κλήση καθολικού στοιχείου που δεν είναι συνάρτηση προκαλεί Phasis\Exceptions\TypeError.

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

Έξοδος κονσόλας

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

Τα console.log, console.info, console.warn, console.error και console.debug δεν εκτυπώνουν. Κάθε κλήση προσθέτει μία μορφοποιημένη γραμμή σε ένα buffer της μηχανής. Το getConsoleLines() επιστρέφει τις αποθηκευμένες γραμμές ως λίστα συμβολοσειρών, το getConsoleOutput() τις επιστρέφει ενωμένες με το \n και το clearConsole() αδειάζει το buffer. Το buffer δεν εκκαθαρίζεται μεταξύ κλήσεων 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

Ορίζει ένα όριο πόρων που μπορεί να ρυθμιστεί κατά την εκτέλεση. Υπάρχει ακριβώς ένα:

Όριο Προεπιλογή Περιγραφή
maxLoopIterations 100000 μέγιστος αριθμός επαναλήψεων ανά εκτέλεση βρόχου for / while / do…while από τον interpreter που διασχίζει το δέντρο
PHP
$engine->setLimit('maxLoopIterations', 1_000_000);

Οποιοδήποτε άλλο όνομα αγνοείται σιωπηρά· δεν υπάρχουν όρια στο μήκος συμβολοσειρών, στην έξοδο κονσόλας ή στον χρόνο εκτέλεσης.

Το όριο βρόχου δεν επιβάλλεται ακόμη παντού. Οι βρόχοι που εκτελούνται στην VM bytecode, η οποία καλύπτει σώματα συναρτήσεων και προγράμματα ανώτατου επιπέδου που δέχεται ο compiler bytecode, δεν καταμετρώνται ποτέ, επομένως το while (true) {} εκτελείται μέχρι να το σταματήσει το PHP (issue #19). Μην βασίζεστε σε αυτό για να σταματήσετε μη έμπιστο κώδικα· χρησιμοποιήστε το max_execution_time του PHP ή ξεχωριστή διεργασία.

Όταν επιτευχθεί το όριο, το σφάλμα μπορεί να συλληφθεί από JS try / catch (ως Error με μήνυμα "Maximum loop iterations exceeded"). Αν δεν το συλλάβει τίποτα, φτάνει στο PHP ως Phasis\Exceptions\InternalError, το οποίο επεκτείνει το Phasis\Exceptions\RuntimeError, όχι το JsThrowable.

Δύο ακόμη ανώτατα όρια είναι ενσωματωμένα στη μηχανή και δεν ρυθμίζονται κατά την εκτέλεση: το Phasis\Runtime\CallStack::maxDepth (4,096 πλαίσια JS) και το guard βάθους για τις μεταγλωττισμένες closures JsToPhp (8,192 πλαίσια PHP). Η υπέρβαση οποιουδήποτε προκαλεί "Maximum call stack size exceeded", το οποίο φτάνει επίσης στο PHP ως InternalError. Τα όρια είναι πολύ υψηλότερα από όσα χρειάζεται οποιοδήποτε νόμιμο πρόγραμμα JS — η αύξησή τους απαιτεί αλλαγή κώδικα, όχι ρύθμιση.

επαναφορά

PHP
public function reset(): void

Εκκαθαρίζει όλα τα καθολικά στοιχεία που έχει ορίσει ο χρήστης και το buffer κονσόλας, και αναδημιουργεί την τυπική βιβλιοθήκη και τον βρόχο συμβάντων.

Σε μηχανή που δημιουργήθηκε στην προεπιλεγμένη τεμπέλικη λειτουργία, το reset() χάνει αυτήν τη στιγμή κάθε ενσωματωμένο στοιχείο που εγκαταστάθηκε τεμπέλικα: μετά, τα Map, JSON, Math, console, fetch και τα υπόλοιπα είναι απροσδιόριστα (issue #22). Μέχρι να διορθωθεί αυτό, δημιουργήστε ένα νέο Engine ή δημιουργήστε το με eager: true, όπου λειτουργεί το reset().

Βρόχος συμβάντων

Η μηχανή περιλαμβάνει βρόχο συμβάντων JavaScript με τα setTimeout, setInterval, clearTimeout, clearInterval και queueMicrotask στο καθολικό αντικείμενο. Οι μικροεργασίες (χειριστές Promise, queueMicrotask) εκτελούνται μέχρι να αδειάσουν μεταξύ κάθε μακροεργασίας· οι χρονοδιακόπτες ενεργοποιούνται με σειρά προθεσμίας μέσω ουράς εργασιών ανά realm.

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

Τα eval() και evalAsModule() καλούν αυτόματα το runEventLoop() μετά την ολοκλήρωση του σύγχρονου κώδικα. Έτσι, ένα script που προγραμματίζει setTimeout(cb, 5000) στο ανώτατο επίπεδο εκτελεί το cb πριν επιστρέψει το eval() — ο βρόχος χρησιμοποιεί εικονικό χρόνο, προχωρώντας το ρολόι στην επόμενη εκκρεμή προθεσμία αντί να περιμένει PHP. Ταιριάζει με τη σημασιολογία του node script.js.

Για ενσωμάτωση σε πραγματικό χρόνο (διακομιστές μακράς διάρκειας, ενσωμάτωση του UI, προγραμματισμός με βάση πραγματικό ρολόι), το tickEventLoop() εκτελεί μία επανάληψη (εκτελεί τις μικροεργασίες μέχρι να αδειάσουν, ενεργοποιεί τυχόν χρονοδιακόπτες που έχουν λήξει) και επιστρέφει. Το pendingTaskCount() αναφέρει τους εκκρεμείς χρονοδιακόπτες· το ενσωματωμένο πρόγραμμα μπορεί να κινεί τη δική του αντλία:

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

Ένα ανεξέλεγκτο setInterval έχει όριο: το runEventLoop() σταματά στις 100,000 επαναλήψεις και προκαλεί απλό \RuntimeException — το σφάλμα του κώδικα JS εμφανίζεται ως σαφές σφάλμα PHP αντί να προκαλέσει πάγωμα. Το διάστημα παραμένει προγραμματισμένο, επομένως κάθε επόμενη κλήση eval() στην ίδια μηχανή προκαλεί το ίδιο σφάλμα· απορρίψτε τη μηχανή μετά. Τα σφάλματα που δεν συλλαμβάνονται σε callback χρονοδιακόπτη εκπέμπονται μέσω του error_log() και ο βρόχος συνεχίζει να αδειάζει· ένας προβληματικός χρονοδιακόπτης δεν τερματίζει το πρόγραμμα.

Άγκιστρα Fetch

Το Phasis περιλαμβάνει ενεργοποιημένο από προεπιλογή το fetch(), με το ext-curl του PHP ως υποκείμενη μεταφορά. Τρία άγκιστρα επιτρέπουν στο ενσωματωμένο πρόγραμμα να αλλάξει τη μεταφορά, να ελέγχει τα αιτήματα ή να ενεργοποιήσει την αποθήκευση cookies. Και τα τρία δέχονται είτε μια κλήσιμη τιμή PHP είτε μια μέθοδο αντικειμένου· δείτε τις Web APIs για τη σύμβαση μορφής τιμών.

setFetchTransport

PHP
public function setFetchTransport(callable $transport): void

Αντικαθιστά το προεπιλεγμένο CurlTransport. Η κλήσιμη τιμή λαμβάνει έναν περιγραφέα αιτήματος και ένα προαιρετικό AbortSignal JsObject· επιστρέφει έναν περιγραφέα απόκρισης.

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

Συνηθισμένο μοτίβο ενσωμάτωσης: εγκαταστήστε μια εικονική μεταφορά στις δοκιμές και την πραγματική μεταφορά curl στην παραγωγή.

setFetchPolicy

PHP
public function setFetchPolicy(callable $hook): void

Άγκιστρο πολιτικής πριν από το αίτημα. Λαμβάνει το Request JsObject· μπορεί να προκαλέσει εξαίρεση για άρνηση, να επιστρέψει τροποποιημένο Request για επανεγγραφή ή να επιστρέψει null/χωρίς τιμή για έγκριση.

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

Προαιρετικό cookie store. Δέχεται οποιοδήποτε αντικείμενο εκθέτει τα get(string $url): string και set(string $url, string $header): void — στην πλευρά PHP ή 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());

Χωρίς το setCookieJar(), το fetch αγνοεί τόσο τις εισερχόμενες κεφαλίδες Set-Cookie όσο και τις εξερχόμενες Cookie — η προεπιλογή είναι χωρίς cookies.

Εξαιρέσεις

Κλάση Επεκτείνει Πότε
Phasis\Exceptions\JsThrowable RuntimeError ρητό JS throw που δεν συλλαμβάνεται
Phasis\Exceptions\TypeError, ReferenceError, RangeError RuntimeError σφάλμα που προκάλεσε η ίδια η μηχανή (π.χ. null.x ή μη ορισμένη μεταβλητή) και δεν συλλαμβάνεται
Phasis\Exceptions\InternalError RuntimeError υπέρβαση του ορίου βρόχου ή της μέγιστης στοίβας κλήσεων
Phasis\Exceptions\RuntimeError \RuntimeException βασική κλάση των παραπάνω
Phasis\Parser\ParseError Phasis\Exceptions\SyntaxError (\RuntimeException) ο πηγαίος κώδικας δεν αναλύεται
\RuntimeException το execFile() δεν μπορεί να διαβάσει το αρχείο· ο βρόχος συμβάντων έφτασε το όριο των 100,000 κύκλων

Για να συλλάβετε κάθε αποτυχία ενός script, συλλάβετε τα Phasis\Exceptions\RuntimeError και Phasis\Exceptions\SyntaxError ή απλώς το \RuntimeException.

Οι εξαιρέσεις που προκαλούν οι δικές σας συναρτήσεις υποδοχής PHP διαφέρουν: προς το παρόν περνούν από τα JS try / catch και φτάνουν στον καλούντα του eval() χωρίς αλλαγή, με την αρχική τους κλάση (issue #20). Δείτε τις Συναρτήσεις υποδοχής.

Κύκλος ζωής

Η δημιουργία του Engine είναι γρήγορη (λίγα ms σε ψυχρή εκκίνηση) και η τυπική βιβλιοθήκη εγκαθίσταται τεμπέλικα. Για τα περισσότερα σενάρια ενσωμάτωσης, αρκεί μία μηχανή μακράς διάρκειας ανά διεργασία. Για χρήση από πολλούς μισθωτές, δώστε σε κάθε μισθωτή ή αίτημα τη δική του μηχανή, ώστε να μη διαρρέουν καθολικά στοιχεία μεταξύ τους. Προς το παρόν, η αξιόπιστη λύση για καθαρή κατάσταση είναι η δημιουργία νέας μηχανής (δείτε το reset).

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