البدء
التثبيت
composer require phasis/phasis
يحتاج Phasis إلى PHP 8.2 أو أحدث مع ثلاث إضافات، وكلها مطلوبة من composer.json:
mbstring— لمعالجة السلاسل.bcmath— لعمليات BigInt الحسابية ومعالجة الأرقام بدقة الأعداد الصحيحة.curl— لنقلfetch()الافتراضي.
تُفعّل mbstring وcurl في معظم إصدارات البناء. أما bcmath فغالبًا لا: إذ تستبعده صور Docker الرسمية لـ php (php:8.x-cli، php:8.x-fpm)، وتوفره بعض التوزيعات في حزمة منفصلة (php-bcmath). من دونه، يرفض Composer تثبيت Phasis. أضفه إلى Dockerfile مبني على الصورة الرسمية:
RUN docker-php-ext-install bcmath
شغّل php -m لمعرفة الإضافات المتاحة في إصدار البناء لديك.
إن ext-intl اختياري. يعمل Phasis من دونه، لكن عائلة Intl.* بأكملها (Collator، NumberFormat، DateTimeFormat، PluralRules، Locale، DisplayNames، ListFormat، RelativeTimeFormat، Segmenter، DurationFormat) وتقويمات Temporal غير التابعة لـ ISO (hebrew، islamic، japanese، persian، إلخ) تحتاج إليه. ثبّته كلما أتاحت التطبيق المضيفة JS تستخدم تنسيقًا يراعي الإعدادات المحلية. كما أن ext-openssl اختياري: تحتاجه عمليات تشفير AES-CBC وAES-CTR وAES-GCM ضمن crypto.subtle، بينما تعمل الملخصات وHMAC من دونه.
لا exec، ولا FFI، ولا Node.js، ولا إضافة أصلية مخصصة.
تشغيل برنامج نصي من CLI
./vendor/bin/phasis -e 'console.log(1 + 2 * 3)'
# 7
./vendor/bin/phasis -e 'console.log([1, 2, 3].map(x => x * x).join(","))'
# 1,4,9
./vendor/bin/phasis path/to/script.js
يطبع CLI ما يكتبه البرنامج النصي باستخدام console.log؛ ولا يطبع قيمة التعبير الأخير.
راجع CLI المرجع للاطلاع على مجموعة الأوامر كاملة.
التضمين في PHP
<?php
require __DIR__ . '/vendor/autoload.php';
use Phasis\Engine;
$engine = new Engine();
// Evaluate an expression
$result = $engine->eval('1 + 2 * 3');
echo $result;
// 7
// Execute a script file
$engine->execFile('path/to/script.js');
// Bridge a PHP value into the JS global scope
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->eval('console.log(config.version)');
// console output is buffered on the engine, not printed
echo $engine->getConsoleOutput();
// 1.0
تحافظ نسخة Engine على حالتها العامة بين الاستدعاءات، لذا ترى الاستدعاءات اللاحقة لـ eval() كل ما سبق تعريفه.
تضيف console.log والأدوات الشقيقة لها البيانات إلى مخزن مؤقت في المحرك بدلاً من كتابتها إلى stdout. اقرأها باستخدام getConsoleOutput() (سلسلة واحدة، والأسطر مفصولة بـ \n) أو getConsoleLines() (قائمة من الأسطر)، وأفرغ المخزن باستخدام clearConsole().
بدء التشغيل البارد قليل التكلفة: لا يُثبّت مسبقًا سوى النواة اللازمة لتحميل المواصفات (Object وFunction وArray وPromise وSymbol وBigInt وRegExp وأنواع الأخطاء). أما Map وTypedArray وTemporal وIntl وURL وfetch وحزمتا Web Platform وFetch الكاملتان، وسائر المكونات، فتُحمّل تلقائيًا عند أول قراءة. مرّر new Engine(eager: true) لتثبيت كل المكونات المضمنة عند الإنشاء؛ يفيد ذلك أدوات التوافق التي تفحص الواصفات قبل قراءتها، ونادرًا ما يلزم للتضمين المعتاد.
استدعاء دوال JS من PHP
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);
// 5
يبحث call() عن الدالة بالاسم في الكائن العام ويستدعيها باستخدام وسيطات PHP المقدمة (مع تحويلها تلقائيًا إلى قيم JS).
إتاحة دالة قابلة للاستدعاء من PHP كدالة JS
$engine->setGlobal('fetchData', function (string $url): array {
return json_decode(file_get_contents($url), true);
});
$engine->eval('const users = fetchData("https://api.example.com/users")');
تُغلّف الإغلاقات في PHP لتصبح دوال JS. تُحوّل وسيطاتها من JS إلى PHP عند الاستدعاء، وتُحوّل قيم الإرجاع من PHP إلى JS.
كائنات PHP
لا يمكن مشاركة كائنات PHP مع JavaScript حتى الآن. يؤدي setGlobal() مع كائن PHP عادي إلى ربط undefined (تتبعه المشكلة #18). مرّر البيانات كمصفوفات تُنسخ إلى JS، وأتح السلوك عبر الإغلاقات:
class Counter {
public int $value = 0;
public function increment(): void { $this->value++; }
}
$counter = new Counter();
$engine->setGlobal('increment', fn () => $counter->increment());
$engine->eval('increment(); increment();');
echo $counter->value;
// 2
راجع الكائنات المشتركة للاطلاع على التفاصيل والحلول البديلة الأخرى.
حدود الموارد
$engine = new Engine();
$engine->setLimit('maxLoopIterations', 100_000);
maxLoopIterations (القيمة الافتراضية 100,000) هو الحد الوحيد القابل للضبط وقت التشغيل، وينطبق حاليًا على الشيفرة التي ينفذها المفسر المتجول في الشجرة فقط. لا تُحتسب الحلقات التي تنفذها آلة البايت كود الافتراضية (أجسام الدوال ومعظم الشيفرة على المستوى الأعلى)، لذا لا يكفي الحد وحده لإيقاف while (true) {} (المشكلة #19). عند بلوغ الحد، يمكن التقاط الخطأ في JS؛ وإذا لم يُلتقط، يصل إلى PHP بوصفه Phasis\Exceptions\InternalError، وهو فئة فرعية من Phasis\Exceptions\RuntimeError.
لدى المحرك أيضًا حد ثابت لمكدس الاستدعاءات يبلغ 4,096 إطارًا (وحاجزًا من 8,192 إطارًا للإغلاقات المحوّلة)، وتجاوز هذا الحد يطلق الخطأ "Maximum call stack size exceeded"، ويكون كذلك من النوع InternalError. لا يوجد حد للوقت الفعلي، لذا شغّل الشيفرة غير الموثوقة ضمن PHP الخاص بها max_execution_time أو في عملية منفصلة. راجع API: setLimit.