
# التكرار الديناميكي على العناصر

استخدم `->for_each()` عندما لا تعرف سيرورة العمل عدد الفروع وقت البناء. تقرأ Queuety مصفوفة من حالة سيرورة العمل، وترسل مهمة فرع لكل عنصر، ولا تتقدم إلا عند استيفاء شرط الإكمال المعيّن.

## الطريقة `->for_each()`

```php
use Queuety\Contracts\ForEachHandler;
use Queuety\Contracts\ForEachReducer;
use Queuety\Enums\ForEachMode;
use Queuety\Queuety;

Queuety::workflow( 'index_site' )
    ->then( PlanPagesStep::class )
    ->for_each(
        items_key: 'pages',
        handler_class: IndexPageStep::class,
        result_key: 'page_results',
        mode: ForEachMode::Quorum,
        quorum: 2,
        reducer_class: IndexSummaryReducer::class,
        name: 'index_pages',
    )
    ->then( PublishIndexStep::class )
    ->dispatch( [ 'site_id' => 42 ] );
```

تكتب خطوة التخطيط `pages` في حالة سيرورة العمل. وتقرأ خطوة التكرار على كل عنصر تلك المصفوفة وترسل مهمة فرع لكل عنصر.

## معالجات الفروع

تطبق معالجات فروع التكرار على كل عنصر `Contracts\ForEachHandler`:

```php
use Queuety\Contracts\ForEachHandler;

final class IndexPageStep implements ForEachHandler {
    public function handle_item( array $state, mixed $item, int $index ): array {
        $page = is_array( $item ) ? $item : [ 'url' => (string) $item ];

        return [
            'url' => $page['url'],
            'title' => fetch_title( $page['url'] ),
            'index' => $index,
        ];
    }

    public function config(): array {
        return [
            'max_attempts' => 3,
            'backoff' => 'exponential',
        ];
    }
}
```

في `config()`، يأخذ `backoff` اسم استراتيجية: `exponential` أو `linear` أو `fixed`. تُسقط حاليًا قائمة التأخيرات لكل محاولة هناك وتتحول إلى إعادات محاولة فورية ([#16](https://github.com/inline0/queuety/issues/16))؛ لذا ضع التأخيرات لكل محاولة في تعريف الخطوة ضمن `backoff`.

يحتوي `$state` على حالة سيرورة العمل كاملة، ويمثل `$item` عنصر التكرار الفردي، ويمثل `$index` موضعه المبدوء من الصفر.

## أوضاع الإكمال

`ForEachMode::All`

لا تكتمل الخطوة إلا بعد نجاح كل الفروع. ويؤدي أي إخفاق دائم في أحد الفروع إلى إخفاق خطوة التكرار على كل عنصر.

`ForEachMode::FirstSuccess`

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

`ForEachMode::Quorum`

تكتمل الخطوة عند نجاح `quorum` من الفروع على الأقل. إذا لم يعد بإمكان الفروع المتبقية تحقيق النصاب، تُخفق سيرورة العمل.

## دوال الاختزال

افتراضيًا، تخزن Queuety تجميعًا منظمًا تحت `result_key`:

- `mode`
- `quorum`
- `total`
- `succeeded`
- `failed`
- `settled`
- `winner`
- `results`
- `failures`

إذا أردت ملخصًا مخصصًا، فمرّر فئة مختزِل تطبّق `Contracts\ForEachReducer`. يظل التجميع مخزنًا تحت `result_key`، وتُدمج المفاتيح التي يعيدها المختزِل في حالة سير العمل بجواره:

```php
use Queuety\Contracts\ForEachReducer;

final class IndexSummaryReducer implements ForEachReducer {
    public function reduce( array $state, array $for_each ): array {
        return [
            'indexed_count' => count( $for_each['results'] ),
            'winning_url' => $for_each['winner']['output']['url'] ?? null,
        ];
    }
}
```

يُدمج ناتج المختزِل في حالة سير العمل مثل أي ناتج آخر لخطوة. ويمكنه أيضًا إرجاع `_next_step` للتفرع بعد اكتمال for-each.

## سلوك إعادة المحاولة

تُعاد محاولة فروع for-each بشكل مستقل وفقًا لـ `config()` الخاص بمعالج الفرع. إذا فشلت خطوة for-each واستدعيت `retry_workflow()`، فإن Queuety يعيد توسيع خطوة for-each ويعيد إلى قائمة الانتظار الفروع المفقودة أو الفاشلة فقط. وتظل نتائج الفروع الناجحة في حالة سير العمل.

## الاختيار بين `parallel()` و`for_each()`

استخدم `parallel()` عندما تكون قائمة الفروع ثابتة في الكود.

استخدم `for_each()` عندما تُكتشف قائمة الفروع أثناء التشغيل من حالة سير العمل، وتحتاج إلى أوضاع اكتمال مثل النجاح الأول أو النصاب.
