
# البرمجيات الوسيطة

تحيط البرمجيات الوسيطة بتنفيذ المهام ضمن مسار عمل يشبه طبقات البصلة. ويمكن لكل وسيط تشغيل منطق قبل المهمة وبعدها، أو إيقاف التنفيذ مبكرًا، أو التقاط الاستثناءات. تتضمن Queuety خمس برمجيات وسيطة مدمجة، وتدعم البرمجيات المخصصة عبر الواجهة `Contracts\Middleware`.

تتوفر البرمجيات الوسيطة [لفئات المهام القابلة للإرسال](https://queuety.inline0.dev/ar/docs/jobs#dispatchable-job-classes) (الفئات التي تطبق `Contracts\Job` مع السمة `Dispatchable`).

## الواجهة `Contracts\Middleware`

```php
use Queuety\Contracts\Middleware;

interface Middleware {
    public function handle( object $job, \Closure $next ): void;
}
```

المعامل `$job` هو مثيل المهمة قيد المعالجة. استدعِ `$next( $job )` لتمرير التنفيذ إلى الوسيط التالي في المسار (أو إلى الطريقة `handle()` الخاصة بالمهمة إذا كان هذا آخر وسيط).

## إضافة برمجيات وسيطة إلى مهمة

عرّف طريقة `middleware()` في فئة المهمة لإرجاع مصفوفة من مثيلات البرمجيات الوسيطة:

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;

class SyncInventoryJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly string $sku,
    ) {}

    public function handle(): void {
        // Sync inventory for this SKU...
    }

    public function middleware(): array {
        return [
            new Timeout( seconds: 30 ),
            new RateLimited( max: 100, window: 60 ),
        ];
    }
}
```

## ترتيب تنفيذ المسار

تُطبّق البرمجيات الوسيطة بترتيب المصفوفة، ويكون أول وسيط هو الطبقة الخارجية. إذا أعادت الطريقة `middleware()` القيمة `[A, B, C]`، فسيكون ترتيب التنفيذ كما يلي:

```
A::handle() -> B::handle() -> C::handle() -> $job->handle()
```

يحيط كل وسيط بالوسيط الذي يليه. يُشغّل `A` أولًا قبل المهمة وآخرًا بعدها، ما يجعله مناسبًا للتوقيت أو لإحاطة التنفيذ بـ try/catch.

## كتابة برمجيات وسيطة مخصصة

طبّق الواجهة `Contracts\Middleware`:

```php
use Queuety\Contracts\Middleware;

class LogExecution implements Middleware {
    public function handle( object $job, \Closure $next ): void {
        $class = get_class( $job );
        error_log( "[Queuety] Starting: {$class}" );

        $start = microtime( true );

        try {
            $next( $job );
        } finally {
            $elapsed = round( microtime( true ) - $start, 3 );
            error_log( "[Queuety] Finished: {$class} in {$elapsed}s" );
        }
    }
}
```

استخدمها في مهمة:

```php
public function middleware(): array {
    return [
        new LogExecution(),
    ];
}
```

### الإيقاف المبكر

يمكن للوسيط تخطي المهمة بالكامل بعدم استدعاء `$next()`:

```php
class SkipOnMaintenanceMode implements Middleware {
    public function handle( object $job, \Closure $next ): void {
        if ( wp_is_maintenance_mode() ) {
            return; // Job silently skipped.
        }

        $next( $job );
    }
}
```

## البرمجيات الوسيطة المضمّنة

### `ThrottlesExceptions`

يحدّ من الاستثناءات الصادرة عن معالج المهام لمنع تتابع الأعطال. إذا ألقى المعالج عددًا كبيرًا جدًا من الاستثناءات خلال فترة زمنية، فسيُرفض تنفيذ المهام اللاحقة لذلك المعالج باستخدام `RuntimeException` بدلًا من معالجتها. يمنع ذلك خدمة خارجية متعطلة من استنفاد جميع محاولات إعادة المحاولة عبر مهام كثيرة دفعة واحدة.

```php
use Queuety\Middleware\ThrottlesExceptions;

public function middleware(): array {
    return [
        new ThrottlesExceptions( max_attempts: 10, decay_minutes: 10 ),
    ];
}
```

| المعامل | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
| `$max_attempts` | `int` | `10` | الحد الأقصى للاستثناءات المسموح بها خلال نافذة التراجع |
| `$decay_minutes` | `int` | `10` | مدة النافذة بالدقائق |

يُتتبّع عدد الاستثناءات لكل فئة معالج في جدول الأقفال الخاص بـ Queuety، وتُشارك البيانات بين جميع عمليات العمال. عند بلوغ الحد، تلقي البرمجيات الوسيطة استثناء `RuntimeException` دون استدعاء المهمة، ويتعامل العامل معه كأي إخفاق آخر: تُحتسب المحاولة ضمن `max_attempts`، وتُعاد محاولة المهمة مع التأخير المعتاد، ثم تُدفن بعد استنفاد محاولاتها.

مثال: إذا تعطلت واجهة API خارجية، فستُعالج الإخفاقات العشرة الأولى بشكل طبيعي (مع زيادة عداد الاستثناءات في كل مرة). وستُخفق المحاولات التالية بسرعة دون استدعاء API إلى أن تمر 10 دقائق دون استثناء جديد؛ وتُعاد النافذة من البداية مع كل استثناء مسجل.

### `RateLimited`

يفرض حدًا أقصى لعدد مرات التنفيذ خلال نافذة زمنية. إذا تجاوز التنفيذ هذا الحد داخل البرمجيات الوسيطة، فسيُلقى استثناء `RateLimitExceededException`؛ ويتعامل العامل معه كمحاولة فاشلة عادية، فيعيد المحاولة مع التأخير المعتاد ويحتسبها ضمن `max_attempts`. بعد أن تسجل البرمجيات الوسيطة الحد لدى محدد معدل العامل، تُعاد المهام اللاحقة للمعالج إلى `pending` قبل تشغيلها أيضًا ما دام الحد قائمًا (راجع [تحديد معدل الطلبات](https://queuety.inline0.dev/ar/docs/features/rate-limiting)).

```php
use Queuety\Middleware\RateLimited;

public function middleware(): array {
    return [
        new RateLimited( max: 60, window: 60 ), // 60 per minute
    ];
}
```

| المعامل | النوع | الوصف |
|---|---|---|
| `$max` | `int` | الحد الأقصى لمرات التنفيذ المسموح بها في النافذة |
| `$window` | `int` | مدة النافذة بالثواني |

يستخدم محدد المعدل مثيل `RateLimiter` الداخلي الخاص بـ Queuety، لذا تُفرض الحدود على جميع عمليات العمال.

### `Timeout`

يضبط مهلة تنفيذ لكل مهمة باستخدام `pcntl_alarm`، ويُفترض أن يلقي `TimeoutException` إذا استغرق تشغيل المهمة وقتًا أطول من اللازم. لا تُفرض المهلة حاليًا ([#10](https://github.com/inline0/queuety/issues/10)): لا يفعّل Queuety الإشارات غير المتزامنة، لذا لا يعمل معالج التنبيه أثناء تنفيذ المهمة. كما تعيد البرمجيات الوسيطة ضبط التنبيه عند انتهائها، ما يلغي تنبيه مهلة المهمة الخاص بالعامل.

```php
use Queuety\Middleware\Timeout;

public function middleware(): array {
    return [
        new Timeout( seconds: 30 ),
    ];
}
```

| المعامل | النوع | الوصف |
|---|---|---|
| `$seconds` | `int` | الحد الأقصى لوقت التنفيذ بالثواني |

يتطلب إضافة PHP الخاصة بـ `pcntl`. إذا لم تتوفر `pcntl`، تتجاوز البرمجيات الوسيطة ذلك بصمت (ولا تُفرض أي مهلة).

### `UniqueJob`

يمنع التنفيذ المتزامن للمهام التي تشترك في المفتاح نفسه. ويستخدم جدول أقفال في قاعدة البيانات لضمان تشغيل مثيل واحد فقط في كل مرة. إذا كان القفل محجوزًا بالفعل، تُتخطى المهمة بصمت (ولا يُعاد تنفيذها).

```php
use Queuety\Middleware\UniqueJob;

public function middleware(): array {
    return [
        new UniqueJob(),                         // Lock key defaults to the job class name
        new UniqueJob( key: 'sync_inventory' ),   // Custom lock key
    ];
}
```

| المعامل | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
| `$key` | `string\|null` | `null` (يستخدم اسم الفئة) | مفتاح القفل لضمان التفرد |

### `WithoutOverlapping`

يمنع تداخل تنفيذ المهام التي تشترك في مفتاح منطقي، مع انتهاء صلاحية القفل تلقائيًا لمنع الأقفال الميتة الناتجة عن تعطل العمال. يشبه `UniqueJob`، لكنه مصمم للعمليات طويلة الأمد التي ينبغي تحرير قفلها تلقائيًا بعد مهلة.

```php
use Queuety\Middleware\WithoutOverlapping;

public function middleware(): array {
    return [
        new WithoutOverlapping( key: 'import_products', release_after: 600 ),
    ];
}
```

| المعامل | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
| `$key` | `string` | (مطلوب) | مفتاح العملية المنطقي |
| `$release_after` | `int` | `300` | أقصى مدة للقفل بالثواني قبل تحريره تلقائيًا |

إذا كان القفل محجوزًا بالفعل، تُتخطى المهمة بصمت. وتُنظف الأقفال منتهية الصلاحية تلقائيًا قبل كل محاولة للحصول على قفل.

## مثال: دمج البرمجيات الوسيطة

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;
use Queuety\Middleware\WithoutOverlapping;

class ImportProductsJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly int $supplier_id,
    ) {}

    public function handle(): void {
        // Fetch and import products from the supplier...
    }

    public function middleware(): array {
        return [
            new Timeout( seconds: 300 ),
            new WithoutOverlapping(
                key: "import_supplier_{$this->supplier_id}",
                release_after: 600,
            ),
            new RateLimited( max: 5, window: 60 ),
        ];
    }
}
```

ستقوم هذه المهمة بما يلي:

1. تُنهى إذا استغرق تشغيلها أكثر من 5 دقائق (Timeout)
2. تُتخطى إذا كان هناك استيراد آخر للمورّد نفسه قيد التشغيل (WithoutOverlapping)
3. يُحدّد معدلها بخمسة استيرادات في الدقيقة عبر جميع المورّدين (RateLimited)
