Συναρτήσεις υποδοχής
Οποιοδήποτε κλείσιμο PHP ή κλήσιμη συνάρτηση που δίνεται στη μηχανή μέσω του setGlobal() γίνεται κλήσιμη συνάρτηση JS. Δεν χρειάζεται χειροκίνητη καταχώριση, σχήμα ή μετατροπή δεδομένων — απλώς περάστε την κλήσιμη συνάρτηση.
Βασικά
$engine->setGlobal('greet', function (string $name): string {
return "Hello, $name!";
});
echo $engine->eval('greet("world")');
// Hello, world!
Όταν το greet("world") εκτελείται στο JS:
- Η συμβολοσειρά
"world"μετατρέπεται από JS σε PHP. - Καλείται το κλείσιμο PHP με το μετατρεμμένο όρισμα.
- Η τιμή επιστροφής μετατρέπεται από PHP ξανά σε JS.
Η μετατροπή ακολουθεί τους κανόνες μετατροπής τιμών. Τα ορίσματα είναι αντίγραφα: ένας πίνακας ή αντικείμενο JS φτάνει ως νέος πίνακας PHP, και οι αλλαγές του στο PHP δεν αλλάζουν την τιμή JS.
Αριθμός ορισμάτων και παράμετροι
Η μηχανή περνά τα ορίσματα JS στην κλήσιμη συνάρτηση με τη σειρά, όπως είναι· δεν εξετάζει την υπογραφή. Οι συνηθισμένες συναρτήσεις αγνοούν τα επιπλέον ορίσματα, ενώ οι μεταβλητές παράμετροι τα συλλέγουν. Τα ορίσματα που λείπουν δεν συμπληρώνονται: αν το JS περάσει λιγότερα από όσα απαιτεί η συνάρτηση, το PHP προκαλεί ArgumentCountError. Ορίστε προεπιλεγμένες τιμές για τις προαιρετικές παραμέτρους.
$engine->setGlobal('format', function (string $template, ...$args): string {
return vsprintf($template, $args);
});
echo $engine->eval('format("%s is %d years old", "Ada", 36)');
// Ada is 36 years old
Μετατροπή τύπων
Τα ορίσματα μετατρέπονται σε PHP με βάση τους κανόνες JS→PHP. Η μηχανή καλεί την κλήσιμη συνάρτησή σας από κώδικα που δηλώνει strict_types=1, επομένως το PHP ελέγχει αυστηρά τους τύπους παραμέτρων scalar και δεν κάνει εξαναγκασμό τύπου: ένας αριθμός JS που δίνεται σε παράμετρο string προκαλεί TypeError. Όπως κάθε εξαίρεση PHP από συνάρτηση υποδοχής, φτάνει στον καλούντα του eval() και όχι στο JS (βλ. παρακάτω).
$engine->setGlobal('len', fn(string $s): int => strlen($s));
$engine->eval('len(42)'); // PHP TypeError: Argument #1 ($s) must be of type string, int given
Κάντε χειροκίνητα μετατροπή τύπου μέσα στο κλείσιμο ((string) $value) ή χρησιμοποιήστε τύπους ένωσης (int|string).
Ρίψη εξαιρέσεων πίσω στο JS
Οι συνηθισμένες εξαιρέσεις PHP που προκαλούνται μέσα σε συνάρτηση υποδοχής δεν μετατρέπονται σήμερα σε σφάλματα JS. Περνούν από κάθε χειριστή JS try / catch και .catch() και φτάνουν στον κώδικα PHP που κάλεσε το eval() ή το call(), διατηρώντας την αρχική τους κλάση (ζήτημα #20). Αυτό περιλαμβάνει τα \Exception, \RuntimeException, \DivisionByZeroError και τα \TypeError και \ArgumentCountError PHP που προκαλούνται από μη έγκυρα ορίσματα.
$engine->setGlobal('divide', function (int $a, int $b): int {
if ($b === 0) {
throw new \DivisionByZeroError('cannot divide by zero');
}
return intdiv($a, $b);
});
try {
$engine->eval('
try { divide(10, 0); } catch (e) { /* never runs */ }
');
} catch (\DivisionByZeroError $e) {
echo $e->getMessage(); // cannot divide by zero
}
Για να προκαλέσετε σφάλμα που μπορεί να πιάσει ο κώδικας JS, ρίξτε μία από τις κλάσεις σφαλμάτων της μηχανής: τα Phasis\Exceptions\TypeError, RangeError ή ReferenceError εμφανίζονται στο JS ως αντίστοιχοι τύποι σφάλματος JS, ενώ κάθε άλλο Phasis\Exceptions\RuntimeError εμφανίζεται ως απλό Error, όλα με το μήνυμά σας.
use Phasis\Exceptions\RangeError;
$engine->setGlobal('divide', function (int $a, int $b): int {
if ($b === 0) {
throw new RangeError('cannot divide by zero');
}
return intdiv($a, $b);
});
$engine->eval('
try {
divide(10, 0);
} catch (e) {
console.log(e.name, e.message); // RangeError cannot divide by zero
}
');
Επιστροφή σύνθετων τιμών
Η τιμή επιστροφής μετατρέπεται σύμφωνα με τους κανόνες PHP→JS. Για να επιστρέψετε αντικείμενο JS literal, επιστρέψτε συσχετιστικό πίνακα PHP:
$engine->setGlobal('lookup', function (string $key): array {
return ['key' => $key, 'value' => 42, 'meta' => ['ts' => time()]];
});
$engine->eval('
const r = lookup("answer");
console.log(r.value, r.meta.ts);
');
Για να επιστρέψετε πίνακα JS, επιστρέψτε διαδοχική λίστα PHP:
$engine->setGlobal('range', fn(int $a, int $b): array => range($a, $b));
$engine->eval('range(1, 5).reduce((a, b) => a + b)'); // 15
Η επιστροφή αντικειμένου PHP, συμπεριλαμβανομένου ενός promise PHP από το ReactPHP, το Amp ή το Guzzle, δίνει JS undefined (βλ. Μετατροπή τιμών).
Ασύγχρονες συναρτήσεις υποδοχής
Οι συναρτήσεις υποδοχής είναι σύγχρονες. Ένα promise PHP ή άλλο thenable αντικείμενο δεν αναμένεται: μετατρέπεται σε undefined, οπότε το await hostFn() επιλύεται σε undefined. Εκτελέστε την εργασία σύγχρονα και επιστρέψτε το αποτέλεσμα· ο κώδικας JS μπορεί και πάλι να κάνει await την κλήση, η οποία τυλίγει την τιμή σε επιλυμένο promise:
$engine->setGlobal('loadBody', function (string $url): string {
return file_get_contents($url);
});
$engine->eval('
async function main() {
const body = await loadBody("https://example.com");
console.log(body.length);
}
main();
');
Ειδικά για το HTTP, το ενσωματωμένο fetch() επιστρέφει ήδη πραγματικό promise JS και μπορεί να δρομολογηθεί μέσω του δικού σας πελάτη PHP με προσαρμοσμένη μεταφορά fetch.
Σημειώσεις απόδοσης
Κάθε κλήση συνάρτησης υποδοχής περνά μία φορά το όριο PHP↔JS για τα ορίσματα και μία για την τιμή επιστροφής. Και οι δύο μετατροπές δημιουργούν αντίγραφα, επομένως το κόστος τους είναι O(size of value) — οι πρωτογενείς τιμές έχουν σταθερό κόστος, ενώ οι πίνακες και τα αντικείμενα κοστίζουν ανάλογα με το μέγεθός τους.
Σε εσωτερικούς βρόχους υψηλής συχνότητας, προτιμήστε μία κλήση που κάνει περισσότερη δουλειά αντί για πολλές μικρές, και αποφύγετε να μεταφέρετε μεγάλους πίνακες όταν αλλάζουν μόνο λίγα πεδία.