
# API

لا يتألف التنفيذ API إلا من صنف واحد: `Phasis\Engine`. وكل ما سواه (`JsValue`، `JsFunction`، `JsObject`، …) داخلي للمحرك، ولا يُتاح إلا لدوال المضيف التي تربط بين PHP↔JS. والسطح العام الآخر الوحيد هو [المحلّل المستقل API](https://phasis.inline0.dev/ar/docs/parser) (`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، وحزمة Fetch — فتُسجّل كأدوات وصول نائبة، ولا تُنشأ فعليًا إلا عند أول قراءة. قراءة المتغير العام شفافة: تعمل `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` وينفّذه كسكربت تقليدي ضمن النطاق العام للمحرك، ثم يشغّل حلقة الأحداث حتى تفرغ. ويعيد قيمة آخر تعبير جرى تقييمه بعد تحويلها إلى PHP (أو `null` إن لم يتضمن البرنامج تعبيرًا أخيرًا). راجع [تحويل القيم](https://phasis.inline0.dev/ar/docs/interop/values) للاطلاع على القواعد.

```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`. راجع [الاستثناءات](#exceptions).

```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: تتاح `import` / `export`، و`await` و`import.meta` على المستوى الأعلى، وتُحل المعرّفات النسبية بالاستناد إلى `$path` (مسار ملف مطلق). يشغّل حلقة الأحداث كما في `eval()` ويعيد دائمًا `null`؛ ويمكن قراءة النتائج عبر المتغيرات العامة أو مخزن وحدة التحكم.

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

لا يحل المحمّل إلا المسارات النسبية والمطلقة؛ راجع [القيود المعروفة](https://phasis.inline0.dev/ar/docs/compatibility/limitations#module-loader).

## execFile

```php
public function execFile(string $path): mixed
```

يقرأ ملفًا من القرص ويمرّر محتواه إلى `eval()`، فيُشغّل كسكربت تقليدي مهما كان امتداده؛ والملف الذي يتضمن `import` / `export` يُصدر خطأ SyntaxError. ويصبح المسار الحقيقي للملف أساسًا لمعرّفات `import()` الديناميكية. استخدم `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` |
| أي كائن أو مولّد أو مورد آخر | `undefined` |

تُنسخ المصفوفات: لا تظهر التغييرات التي يجريها JS فيها في مصفوفة PHP. ولا يمكن تمرير كائنات PHP عادية حاليًا ([المشكلة #18](https://github.com/inline0/phasis/issues/18))؛ راجع [الكائنات المشتركة](https://phasis.inline0.dev/ar/docs/interop/shared-objects) للحلول البديلة.

```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` لا تطبع شيئًا. يضيف كل استدعاء سطرًا منسقًا إلى مخزن مؤقت في المحرك. يعيد `getConsoleLines()` الأسطر المخزنة كقائمة من السلاسل النصية، ويعيدها `getConsoleOutput()` مجمّعة مع `\n`، بينما يفرّغ `clearConsole()` المخزن المؤقت. لا يُمسح المخزن المؤقت بين استدعاءات `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` بواسطة المفسّر الذي يتبع بنية الشجرة |

```php
$engine->setLimit('maxLoopIterations', 1_000_000);
```

يُتجاهل أي اسم آخر بصمت؛ فلا توجد حدود لطول السلسلة النصية أو مخرجات وحدة التحكم أو وقت التنفيذ الفعلي.

لم يُطبّق حدّ التكرار في كل المواضع بعد. لا تُحتسب حلقات آلة البايت كود، التي تشمل أجسام الدوال والبرامج العليا التي يقبلها مُصرّف البايت كود؛ لذا يواصل `while (true) {}` التنفيذ حتى يوقفه PHP ([المشكلة #19](https://github.com/inline0/phasis/issues/19)). لا تعتمد عليه لإيقاف الشيفرة غير الموثوقة؛ استخدم `max_execution_time` الخاص بـPHP أو عملية منفصلة.

يمكن التقاط الخطأ الناتج عند بلوغ الحد بواسطة `try` / `catch` في JS (على هيئة `Error` برسالة "Maximum loop iterations exceeded"). وإذا لم يلتقطه شيء، يصل إلى PHP على هيئة `Phasis\Exceptions\InternalError`، الذي يرث من `Phasis\Exceptions\RuntimeError`، وليس من `JsThrowable`.

هناك حدّان إضافيان مضمّنان في المحرك ولا يمكن تغييرهما أثناء التشغيل: `Phasis\Runtime\CallStack::maxDepth` (4,096 إطارًا لـ JS) وحارس عمق الإغلاق المحوّل عبر JsToPhp (8,192 إطارًا لـ PHP). يؤدي تجاوز أي منهما إلى طرح "Maximum call stack size exceeded"، والذي يصل إلى PHP على هيئة `InternalError` أيضًا. وهما أعلى بكثير مما يحتاج إليه أي برنامج JS مشروع؛ وتغييرهما يتطلب تعديل الكود، لا إعدادًا.

## إعادة الضبط

```php
public function reset(): void
```

يمسح جميع المتغيرات العامة التي عرّفها المستخدم ومخزن وحدة التحكم المؤقت، ويعيد إنشاء المكتبة القياسية وحلقة الأحداث.

في المحرك المُنشأ بوضع التحميل الكسول الافتراضي، يفقد `reset()` حاليًا كل العناصر المضمّنة التي ثُبّتت بتحميل كسول؛ وبعدها تصبح `Map` و`JSON` و`Math` و`console` و`fetch` وغيرها غير معرّفة ([المشكلة #22](https://github.com/inline0/phasis/issues/22)). إلى أن يُصلح ذلك، أنشئ `Engine` جديدًا بدلًا منه، أو أنشئه باستخدام `eager: true`، حيث تعمل `reset()`.

## حلقة الأحداث

يتضمن المحرك حلقة أحداث JavaScript مع `setTimeout` و`setInterval` و`clearTimeout` و`clearInterval` و`queueMicrotask` على الكائن العام. تُنفّذ المهام الدقيقة (معالجات Promise و`queueMicrotask`) بين كل مهمة كبرى؛ وتُطلق المؤقتات حسب ترتيب مواعيدها عبر قائمة مهام خاصة بكل مجال.

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

يستدعي `eval()` و`evalAsModule()` تلقائيًا `runEventLoop()` بعد اكتمال الكود المتزامن. لذا، فإن البرنامج النصي الذي يجدول `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` عاديًا — فيظهر خطأ PHP واضح بدلًا من تعليق ناتج عن خطأ في كود JS. ويبقى الفاصل الزمني مجدولًا، لذلك يطرح كل استدعاء لاحق لـ `eval()` على المحرك نفسه الخطأ ذاته؛ تخلّص من المحرك بعد ذلك. وتُصدر الأخطاء غير الملتقطة في رد نداء المؤقت عبر `error_log()`، وتواصل الحلقة تفريغ المهام؛ فمؤقت واحد معطوب لا يوقف البرنامج.

## خطافات Fetch

يأتي Phasis مع تفعيل `fetch()` افتراضيًا، ويستخدم `ext-curl` الخاص بـ PHP كوسيلة نقل أساسية. تتيح ثلاثة خطافات للمضيف تبديل وسيلة النقل أو التحكم في الطلبات أو تفعيل تخزين ملفات تعريف الارتباط. تقبل الخطافات الثلاثة إما دالة قابلة للاستدعاء من PHP أو طريقة على كائن؛ راجع [واجهات Web APIs](https://phasis.inline0.dev/ar/docs/compatibility/web-apis) للاطلاع على مواصفات شكل القيم.

### setFetchTransport

```php
public function setFetchTransport(callable $transport): void
```

استبدل CurlTransport الافتراضي. تتلقى الدالة واصف طلب وكائن JsObject اختياريًا من `AbortSignal`، وتُعيد واصف استجابة.

```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
```

خطاف سياسة يُنفّذ قبل الطلب. يتلقى كائن JsObject من `Request`؛ ويمكنه طرح خطأ للرفض، أو إعادة `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
```

مخزن اختياري لملفات تعريف الارتباط. يقبل أي كائن يوفّر `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` الصادرة — والإعداد الافتراضي هو **عدم استخدام ملفات تعريف الارتباط**.

## الاستثناءات

| الفئة | يرث من | متى |
|---|---|---|
| `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()` قراءة الملف؛ بلغ event loop الحد الأقصى البالغ 100,000 دورة |

لالتقاط كل حالات فشل البرنامج النصي، التقط `Phasis\Exceptions\RuntimeError` و`Phasis\Exceptions\SyntaxError`، أو استخدم ببساطة `\RuntimeException`.

تختلف الاستثناءات التي تطرحها دوال المضيف PHP الخاصة بك؛ إذ تمر حاليًا عبر `try` / `catch` في JS وتصل إلى مستدعي `eval()` دون تغيير، مع الاحتفاظ بفئتها الأصلية ([المشكلة #20](https://github.com/inline0/phasis/issues/20)). راجع [دوال المضيف](https://phasis.inline0.dev/ar/docs/interop/host-functions#throwing-back-into-js).

## دورة الحياة

إن إنشاء `Engine` سريع (بضعة مللي ثوانٍ عند التشغيل البارد)، وتُثبّت المكتبة القياسية بتحميل كسول. تكفي وحدة محرك واحدة طويلة الأمد لكل عملية في معظم سيناريوهات التضمين. أما الاستخدام متعدد المستأجرين، فأنشئ محركًا لكل مستأجر أو طلب كي لا تتسرب المتغيرات العامة فيما بينهم. ويظل إنشاء محرك جديد الوسيلة الموثوقة للحصول على حالة نظيفة حاليًا (راجع [reset](#reset)).

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