دوال المضيف
يصبح أي إغلاق أو دالة قابلة للاستدعاء في 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 من أنواع المعاملات العددية بدقة ولا تجري تحويلًا ضمنيًا: العدد في 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 حرفي، أعد مصفوفة ترابطية من 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، بما في ذلك وعد PHP من ReactPHP أو Amp أو Guzzle، يعطي JS القيمة undefined (انظر تحويل القيم).
دوال المضيف غير المتزامنة
دوال المضيف متزامنة. لا يُنتظر وعد PHP أو أي كائن thenable آخر؛ بل يتحول إلى undefined، لذا يُحل await hostFn() إلى undefined. نفّذ العمل بشكل متزامن وأعد النتيجة؛ مع ذلك، يمكن لشيفرة JS استخدام await للاستدعاء، ما يغلّف القيمة في وعد محلول:
$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() المضمنة بالفعل وعد JS حقيقيًا، ويمكن توجيهها عبر عميل PHP الخاص بك باستخدام نقل جلب مخصص.
ملاحظات الأداء
يعبر كل استدعاء لدالة مضيف حدود PHP↔JS مرةً للوسيطات ومرةً للقيمة المعادة. ينسخ التحويل في الحالتين، لذا تكون الكلفة O(size of value) — فالأنواع البدائية تستغرق وقتًا ثابتًا، بينما تتناسب كلفة المصفوفات والكائنات مع حجمها.
في الحلقات الداخلية كثيرة التكرار، فضّل استدعاءً واحدًا ينجز عملًا أكثر على استدعاءات صغيرة كثيرة، وتجنب تمرير مصفوفات كبيرة ذهابًا وإيابًا إذا لم يتغير سوى بضعة حقول.