بناء سير عمل موجّه بالأحداث
نسّق الخدمات عبر الأحداث والعمليات القابلة للتكرار الآمن
بناء سير عمل موجّه بالأحداث درس مجاني في PHP Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في PHP Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة PHP Academy 4 دروس في المجموع.
سير العمل القائم على الأحداث
الرسالة المفردة سهلة. أما سير العمل — «وضع الطلب → حجز المخزون → خصم قيمة البطاقة → الشحن → الإخطار» عبر عدة خدمات — فهو المجال الذي يثبت فيه التصميم القائم على الأحداث قيمته، وقد يسبب لكم مشكلات إن نُفّذ بسذاجة.
يغطي هذا الدرس التنسيق اللامركزي مقابل التنسيق المركزي، ونمط outbox للنشر الذري، وأنماط saga للتراجع الموزع، وعدم التأثر بالتكرار الذي يحافظ على تماسك كل ذلك.
التنسيق اللامركزي مقابل التنسيق المركزي
طريقتان لتنسيق التدفقات متعددة الخطوات:
- التنسيق اللامركزي — تتفاعل كل خدمة مع الأحداث وتصدر أحداثها الخاصة، من دون عقل مركزي. تكون الخدمات مترابطة بشكل فضفاض، لكن التدفق العام يكون ضمنيًا ويصعب تتبعه.
- التنسيق المركزي — يحدد منسق مركزي لكل خدمة ما ينبغي أن تفعله بعد ذلك. يكون التدفق واضحًا وقابلًا للمراقبة، لكن المنسق يصبح نقطة اقتران.
قاعدة عامة: استخدموا التنسيق اللامركزي للتوزيع البسيط، والتنسيق المركزي عندما يتضمن التدفق خطوات مرتبة كثيرة ويحتاج إلى حالة واضحة.
الأحداث مقابل الأوامر
سمّوا رسائلكم بعناية:
- يعبّر الحدث عن حقيقة وقعت في الماضي:
OrderPlaced. ولا يهتم الناشر بمن يستمع. - يطلب الأمر تنفيذ إجراء مستقبلي لدى معالج محدد:
ChargeCard.
تقود الأحداث التنسيق اللامركزي، بينما تقود الأوامر التنسيق المركزي. ويُعد خلط المصطلحين، مثل تسمية رسالة «حدث» مع توقعها سرًا وجود معالج واحد، مصدرًا شائعًا للاقتران الخفي.
<?php
final class OrderPlaced {
public function __construct(
public readonly string $orderId,
public readonly string $customerId,
public readonly int $amountCents,
public readonly string $occurredAt,
) {}
}
$e = new OrderPlaced('o-42', 'c-7', 1990, gmdate('c'));
echo json_encode($e), "\n";مشكلة الكتابة المزدوجة
الخطأ الكلاسيكي هو أن يعّدل معالج قاعدة البيانات وينشر رسالة باعتبارهما عمليتين منفصلتين. إذا توقفت العملية بينهما، يحدث عدم اتساق — فقد يتغير الصف من دون إرسال حدث، أو يحدث العكس.
<?php
// BROKEN: not atomic. A crash between the two lines corrupts state.
function placeOrder(PDO $db, $broker, array $o): void {
$db->prepare('INSERT INTO orders ...')->execute($o);
// <-- crash here = row exists but no event ever published
$broker->publish('OrderPlaced', json_encode($o));
}نمط outbox المعاملي
يكمن الحل في نمط outbox: أدرج الحدث في جدول outbox ضمن معاملة قاعدة البيانات نفسها التي تغيّر بياناتك. ثم تقرأ عملية ترحيل منفصلة الصفوف غير المنشورة وترسلها إلى وسيط الرسائل. التزام ذري واحد، ومن دون كتابة مزدوجة.
<?php
function placeOrder(PDO $db, array $o): void {
$db->beginTransaction();
$db->prepare('INSERT INTO orders (id, total) VALUES (?, ?)')
->execute([$o['id'], $o['total']]);
// Same transaction -> atomic with the business write
$db->prepare('INSERT INTO outbox (id, type, payload) VALUES (?, ?, ?)')
->execute([bin2hex(random_bytes(8)), 'OrderPlaced', json_encode($o)]);
$db->commit();
}المرحّل (الناشر)
تستطلع عملية عاملة جدول outbox، أو تتابع سجل تغييرات قاعدة البيانات عبر CDC، وتنشر كل صف ثم تعلّمه على أنه أُرسل. ولأن المرحّل قد يتعطل بعد النشر وقبل وضع علامة الإرسال، فإنه يعمل هو نفسه وفق دلالة التسليم مرة واحدة على الأقل — وهذا مقبول لأن المستهلكين لا يتأثرون بالتكرار.
<?php
function relayOutbox(PDO $db, $broker): void {
$rows = $db->query(
'SELECT id, type, payload FROM outbox
WHERE published_at IS NULL ORDER BY created_at LIMIT 100'
)->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
$broker->publish($r['type'], $r['payload'], messageId: $r['id']);
$db->prepare('UPDATE outbox SET published_at = now() WHERE id = ?')
->execute([$r['id']]);
}
}المستهلكون غير المتأثرين بالتكرار مجددًا
بما أن المرحّل ووسيط الرسائل يعملان وفق دلالة التسليم مرة واحدة على الأقل، فسترى المعالجات اللاحقة رسائل مكررة. يسجّل كل مستهلك معرّف الرسالة التي عالجها، ويتجاوز التكرارات — وهي بوابة إزالة التكرار نفسها التي تعلمتموها سابقًا، وتُطبَّق الآن لكل خدمة.
<?php
function onOrderPlaced(PDO $db, string $messageId, array $data): void {
$db->beginTransaction();
try {
$db->prepare('INSERT INTO inbox (message_id) VALUES (?)')
->execute([$messageId]); // unique index = dedup
} catch (PDOException $e) {
$db->rollBack();
return; // already handled this message
}
reserveStock($data['orderId']);
$db->commit();
}
function reserveStock(string $id): void {}أنماط Saga: التراجع الموزع
لا يمكنكم فتح معاملة ACID واحدة عبر الخدمات. يمثّل نمط saga تدفقًا طويل التشغيل كسلسلة من المعاملات المحلية، لكل منها إجراء تعويضي يعكس أثرها. إذا فشلت الخطوة 3، فشغّلوا الإجراءات التعويضية للخطوتين 2 و1 بترتيب عكسي.
مثال: فشل الدفع بعد حجز المخزون → أصدِر ReleaseStock للتعويض. لا يوجد تراجع تلقائي — عليكم تصميم إجراء الإلغاء لكل خطوة.
Saga منسّقة
يقود المنسق الـ saga؛ إذ يتقدم عند النجاح ويرسل الإجراءات التعويضية عند الفشل. احفظوا حالة الـ saga حتى يمكن استئنافها بعد حدوث عطل.
<?php
function handleStepResult(array $saga, string $step, bool $ok, $bus): array {
if ($ok) {
$next = ['reserveStock' => 'chargeCard', 'chargeCard' => 'ship'][$step] ?? null;
if ($next) { $bus->send($next, $saga['orderId']); $saga['state'] = $next; }
else { $saga['state'] = 'completed'; }
} else {
// Run compensations in reverse for whatever already succeeded
foreach (array_reverse($saga['done']) as $s) {
$bus->send('compensate.' . $s, $saga['orderId']);
}
$saga['state'] = 'compensating';
}
return $saga;
}المهلات الزمنية في التدفقات الطويلة
قد لا تعيد إحدى خطوات الـ saga أي نتيجة على الإطلاق — فقد تكون خدمة الدفع متوقفة، أو لا يأتي اعتماد بشري أبدًا. ومن دون مهلة زمنية، يظل الـ saga عالقًا إلى الأبد محتفظًا بالحجوزات. احفظوا موعدًا نهائيًا لكل خطوة؛ وامنحوا مجدولًا مهمة البحث عن أنماط saga المتأخرة وتشغيل مسار الفشل أو التعويض.
<?php
function reapTimedOutSagas(PDO $db, $bus): void {
$rows = $db->query(
"SELECT order_id, state FROM sagas
WHERE state NOT IN ('completed','compensating')
AND deadline_at < now()"
)->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
echo "Saga {$r['order_id']} timed out at step {$r['state']}\n";
$bus->send('saga.compensate', $r['order_id']); // trigger rollback
}
}إدارة الإصدارات وقابلية الرصد
تستمر مسارات العمل لسنوات؛ لذا يجب أن تتطور الأحداث بأمان:
- أضيفوا
version(أو مخططًا) إلى كل حدث؛ وعلى المستهلكين تحمّل الحقول الجديدة غير المعروفة وألا يفترضوا وجود أي حقل مطلقًا. - فضّلوا التغييرات الإضافية؛ ولا تعيدوا استخدام معنى حقل موجود لغرض آخر مطلقًا.
- مرّروا معرّف ارتباط عبر كل رسالة حتى تتمكنوا من تتبّع معاملة أعمال واحدة عبر جميع الخدمات في سجلاتكم/تتبّعكم.
من دون معرّفات الارتباط، يصبح تصحيح أخطاء مسار عمل منسّق عبر خمس خدمات شبه مستحيل.
<?php
$envelope = [
'type' => 'OrderPlaced',
'version' => 2,
'correlationId' => $incoming['correlationId'] ?? bin2hex(random_bytes(8)),
'occurredAt' => gmdate('c'),
'data' => ['orderId' => 'o-42'],
];
echo json_encode($envelope, JSON_PRETTY_PRINT), "\n";تحقّق سريع
تجنّب مشكلة الكتابة المزدوجة.
مراجعة
يمكنكم الآن تصميم مسارات عمل موثوقة قائمة على الأحداث:
- اختاروا التنسيق اللامركزي (الأحداث) أو التنسيق المركزي (الأوامر) بحسب تعقيد كل مسار.
- حلّوا مشكلة الكتابة المزدوجة باستخدام صندوق الصادر المعاملي ومرحّل.
- اجعلوا كل مستهلك عديم التأثير عند التكرار عبر صندوق وارد/مفتاح لإزالة التكرار.
- استخدموا الساغات مع إجراءات تعويضية لتنفيذ التراجع الموزّع.
- طوّروا إصدارات الأحداث بإضافات فقط، ومرّروا معرّف ارتباط لضمان قابلية التتبع.
تحوّل هذه الأنماط الرسائل غير المترابطة إلى عمليات أعمال موثوقة وقابلة للرصد.
الأسئلة الشائعة
هل درس «بناء سير عمل موجّه بالأحداث» مجاني؟
نعم — نص درس «بناء سير عمل موجّه بالأحداث» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة PHP Academy، انتقل إلى CoddyKit PRO. تتضمن دورة PHP Academy 4 دروس في المجموع.
ماذا ستتعلم في «بناء سير عمل موجّه بالأحداث»؟
نسّق الخدمات عبر الأحداث والعمليات القابلة للتكرار الآمن تتمرن على PHP Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ PHP Academy؟
لا تُشترط خبرة سابقة. PHP Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «بناء سير عمل موجّه بالأحداث»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس PHP Academy هذا؟
نعم. كل درس في PHP Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- لماذا نستخدم المراسلة غير المتزامنة
- العمل مع RabbitMQ في PHP
- Apache Kafka مع PHP
- بناء سير عمل موجّه بالأحداث