API
Η επιφάνεια εκτέλεσης API αποτελείται από μία κλάση: Phasis\Engine. Όλα τα υπόλοιπα (JsValue, JsFunction, JsObject, …) είναι εσωτερικά της μηχανής και εκτίθενται μόνο σε συναρτήσεις υποδοχής που γεφυρώνουν τα PHP↔JS. Η μόνη άλλη δημόσια επιφάνεια είναι ο αυτόνομος parser API (οι κλάσεις Phasis\Parser\Parser και Phasis\Ast), που αναλύει τον πηγαίο κώδικα χωρίς να τον εκτελεί.
Δημιουργία
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 να βλέπουν τις μορφές περιγραφέα που αναμένουν.
αξιολόγηση
public function eval(string $source): mixed
Αναλύει και εκτελεί το $source ως κλασικό script στο καθολικό πεδίο της μηχανής και, στη συνέχεια, εκτελεί τον βρόχο συμβάντων μέχρι να αδειάσει. Επιστρέφει την τιμή της τελευταίας αξιολογημένης έκφρασης, μετατρεπόμενη σε PHP (ή null αν το πρόγραμμα δεν είχε τελική έκφραση). Δείτε τους κανόνες στη Μετατροπή τιμών.
$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():
$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. Δείτε τις Εξαιρέσεις.
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
Αξιολογεί το $source ως ES module: είναι διαθέσιμα τα import / export και τα await και import.meta ανώτατου επιπέδου, ενώ οι σχετικοί προσδιοριστές επιλύονται με βάση το $path (απόλυτη διαδρομή αρχείου). Εκτελεί τον βρόχο συμβάντων όπως το eval() και επιστρέφει πάντα το null· διαβάστε τα αποτελέσματα μέσω καθολικών στοιχείων ή του buffer κονσόλας.
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
Ο φορτωτής επιλύει μόνο σχετικές και απόλυτες διαδρομές· δείτε τους Γνωστούς περιορισμούς.
execFile
public function execFile(string $path): mixed
Διαβάζει ένα αρχείο από τον δίσκο και περνά τα περιεχόμενά του στο eval(), ώστε να εκτελεστεί ως κλασικό script ανεξάρτητα από την επέκτασή του· ένα αρχείο με import / export αποτυγχάνει με SyntaxError. Η πραγματική διαδρομή του αρχείου γίνεται η βάση για δυναμικούς προσδιοριστές import(). Για αρχεία module χρησιμοποιήστε το evalAsModule().
$engine->execFile(__DIR__ . '/script.js');
Αν δεν είναι δυνατή η ανάγνωση του αρχείου, το PHP εμφανίζει προειδοποίηση και το execFile() προκαλεί απλό \RuntimeException ("Cannot read file: …").
setGlobal
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)· δείτε τις Κοινόχρηστες οντότητες για εναλλακτικές λύσεις.
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
Μέσα στο JS:
console.log(config.version); // "1.0"
log('hello'); // calls the PHP closure
κλήση
public function call(string $name, mixed ...$args): mixed
Καλεί μια καθολική συνάρτηση JS με ορίσματα PHP (που μετατρέπονται όπως για το setGlobal). Η τιμή επιστροφής μετατρέπεται ξανά σε PHP. Η κλήση ονόματος που δεν έχει οριστεί προκαλεί Phasis\Exceptions\ReferenceError· η κλήση καθολικού στοιχείου που δεν είναι συνάρτηση προκαλεί Phasis\Exceptions\TypeError.
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3); // 5
Έξοδος κονσόλας
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().
$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
Ορίζει ένα όριο πόρων που μπορεί να ρυθμιστεί κατά την εκτέλεση. Υπάρχει ακριβώς ένα:
| Όριο | Προεπιλογή | Περιγραφή |
|---|---|---|
maxLoopIterations |
100000 |
μέγιστος αριθμός επαναλήψεων ανά εκτέλεση βρόχου for / while / do…while από τον interpreter που διασχίζει το δέντρο |
$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 — η αύξησή τους απαιτεί αλλαγή κώδικα, όχι ρύθμιση.
επαναφορά
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.
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() αναφέρει τους εκκρεμείς χρονοδιακόπτες· το ενσωματωμένο πρόγραμμα μπορεί να κινεί τη δική του αντλία:
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
public function setFetchTransport(callable $transport): void
Αντικαθιστά το προεπιλεγμένο CurlTransport. Η κλήσιμη τιμή λαμβάνει έναν περιγραφέα αιτήματος και ένα προαιρετικό AbortSignal JsObject· επιστρέφει έναν περιγραφέα απόκρισης.
// 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
public function setFetchPolicy(callable $hook): void
Άγκιστρο πολιτικής πριν από το αίτημα. Λαμβάνει το Request JsObject· μπορεί να προκαλέσει εξαίρεση για άρνηση, να επιστρέψει τροποποιημένο Request για επανεγγραφή ή να επιστρέψει null/χωρίς τιμή για έγκριση.
// 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
Προαιρετικό cookie store. Δέχεται οποιοδήποτε αντικείμενο εκθέτει τα get(string $url): string και set(string $url, string $header): void — στην πλευρά PHP ή 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());
Χωρίς το 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).
foreach ($requests as $req) {
$engine = new Engine();
$engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
$engine->execFile($req->scriptPath);
}