التكرار الديناميكي على العناصر
استخدم ->for_each() عندما لا تعرف سيرورة العمل عدد الفروع وقت البناء. تقرأ Queuety مصفوفة من حالة سيرورة العمل، وترسل مهمة فرع لكل عنصر، ولا تتقدم إلا عند استيفاء شرط الإكمال المعيّن.
الطريقة ->for_each()
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:
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)؛ لذا ضع التأخيرات لكل محاولة في تعريف الخطوة ضمن backoff.
يحتوي $state على حالة سيرورة العمل كاملة، ويمثل $item عنصر التكرار الفردي، ويمثل $index موضعه المبدوء من الصفر.
أوضاع الإكمال
ForEachMode::All
لا تكتمل الخطوة إلا بعد نجاح كل الفروع. ويؤدي أي إخفاق دائم في أحد الفروع إلى إخفاق خطوة التكرار على كل عنصر.
ForEachMode::FirstSuccess
تكتمل الخطوة بمجرد نجاح أول فرع. وتُدفن الفروع النشطة المتبقية بوصفها متجاوزة.
ForEachMode::Quorum
تكتمل الخطوة عند نجاح quorum من الفروع على الأقل. إذا لم يعد بإمكان الفروع المتبقية تحقيق النصاب، تُخفق سيرورة العمل.
دوال الاختزال
افتراضيًا، تخزن Queuety تجميعًا منظمًا تحت result_key:
modequorumtotalsucceededfailedsettledwinnerresultsfailures
إذا أردت ملخصًا مخصصًا، فمرّر فئة مختزِل تطبّق Contracts\ForEachReducer. يظل التجميع مخزنًا تحت result_key، وتُدمج المفاتيح التي يعيدها المختزِل في حالة سير العمل بجواره:
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() عندما تُكتشف قائمة الفروع أثناء التشغيل من حالة سير العمل، وتحتاج إلى أوضاع اكتمال مثل النجاح الأول أو النصاب.