فخ خفي في Laravel: إرسال Events داخل الـ Transaction

فخ خفي في Laravel: إرسال Events داخل الـ Transaction

عند العمل في Laravel، قد تقع في خطأ غير واضح من النظرة الأولى:
إرسال Event من داخل Database Transaction على افتراض أن كل شيء مترابط وآمن.
لكن الحقيقة أن Laravel ينفذ الـ Event مباشرة، حتى لو فشلت الترانزكشن لاحقًا وتم تنفيذ Rollback.

هذا يؤدي إلى مشكلة خطيرة، وهي أن الـ Listener قد ينفذ إجراءات اعتمادًا على بيانات لم يتم حفظها في قاعدة البيانات أصلًا.

على سبيل المثال:

  • قاعدة البيانات تنفذ Rollback بسبب فشل المعاملة.

  • لكن الـ Listener كان قد أرسل بريدًا إلكترونيًا، أو شغّل Job، أو نفّذ خطوة تعتمد على بيانات غير موجودة.

هذه حالة عدم تناسق قد تسبب أخطاء صعبة التعقب.


تشبيه مبسط

تخيل أنك في مطعم:

  • الويتر (Laravel) يأخذ الطلب من الكاشير (Database Transaction).

  • قبل أن يسجل الكاشير الطلب رسميًا، يذهب الويتر إلى المطبخ ويطلب من الشيف البدء في تجهيز الطعام (Event Listener).

  • بعد لحظات يخبره الكاشير بأن العميل ألغى الطلب.

  • لكن الطعام بدأ إعداده بالفعل.

هذا بالضبط ما يحدث إذا تم إرسال Event قبل اكتمال عملية الـ Commit.


ما الذي يحدث فعليًا؟

إذا قمت بإرسال Event داخل ترانزكشن مثل:

DB::transaction(function () { Order::create($data); event(new OrderCreated()); });

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


الحل الصحيح

يوفر Laravel طريقتين أساسيتين لضمان تنفيذ الـ Event بعد نجاح عملية الـ Commit فقط.

1. استخدام DB::afterCommit()

DB::transaction(function () { Order::create($data); DB::afterCommit(function () { event(new OrderCreated()); }); });

سيتم تنفيذ الكود هنا فقط إذا نجحت الترانزكشن.
وفي حالة حدوث Rollback، لن يتم تنفيذ أي شيء.


2. تفعيل خاصية $afterCommit داخل Listener

class SendNotification { public $afterCommit = true; public function handle(OrderCreated $event) { } }

بهذه الطريقة، لن يتم تنفيذ الـ Listener إلا بعد نجاح الـ Commit.


الخلاصة

إرسال Events داخل الترانزكشن قد يبدو منطقيًا، لكنه قد يسبب آثارًا جانبية خطيرة إذا فشلت المعاملة.
باستخدام DB::afterCommit() أو تفعيل $afterCommit داخل الـ Listener، تضمن أن المنطق الخاص بك لا يعمل إلا بعد نجاح عملية الحفظ.

هذه من أفضل الممارسات التي تجعل نظامك أكثر موثوقية واتساقًا في بيئة الإنتاج.