يا جماعة الخير، السلام عليكم ورحمة الله.
اسمحوا لي أحكي لكم قصة صارت معي قبل كم سنة. كان عنا مشروع متجر إلكتروني كبير، وكل الفريق شغالين عليه ليل نهار. الكود كان “نظيف” على حسب كل الكتب والمقالات: Controllers مرتبة، Services مقسمة صح، وعلاقات Eloquent مكتوبة بحرفنة. أطلقنا المشروع، والأمور كانت تمام في أول أسبوع.
بعدها، بلشت الشكاوى توصل من العميل. “الموقع بطيء”، “الصفحة الرئيسية بتعلّق أوقات الذروة”. فتحنا الـ Logs، ما في أي أخطاء. فحصنا الاستعلامات، كلها سريعة وفيها eager loading وما في أي مشكلة N+1. صرنا نلوم السيرفر، وبعدها زدنا الـ RAM، وبعدها قلنا يمكن المشكلة من Redis… جربنا كل الحلول التقليدية. وصلنا لمرحلة من الإحباط، لدرجة إنه واحد من الفريق قال مازحًا: “شكلها عين وصابتنا!”.
في ليلة من الليالي، وأنا قاعد بقلّب في الكود للمرة المليون، خطرت ببالي فكرة. المشكلة مش باللي بنطلبه من قاعدة البيانات، المشكلة باللي بيعمله Laravel بعد ما توصل البيانات. ومن هداك اليوم، تغيرت طريقة تفكيري في بناء تطبيقات Laravel للأبد.
العارض الذي حيّر الفريق: كل شيء “صحيح” لكن التطبيق ينهار
دعونا نحلل سيناريو يتكرر في أغلب المشاريع المتوسطة والكبيرة. لديك لوحة تحكم (Dashboard) بسيطة تعرض بعض المعلومات الحيوية:
- آخر 20 طلبًا في النظام.
- قائمة المستخدمين النشطين حاليًا.
- بعض الإحصائيات السريعة عن المبيعات اليومية.
الكود المسؤول عن جلب آخر الطلبات قد يبدو بريئًا تمامًا، شيء من هذا القبيل:
// في الـ Controller أو الـ Service
$orders = Order::with('user')->latest()->take(20)->get();
// ثم تمريره إلى الـ view
return view('dashboard', ['orders' => $orders]);
عندما تنظر إلى هذا الكود، كل شيء يبدو مثاليًا:
- لا توجد مشكلة N+1: لقد استخدمنا
with('user')لجلب المستخدمين المرتبطين باستعلام واحد إضافي فقط. - لا توجد استعلامات معقدة: مجرد ترتيب وجلب لآخر 20 سجل.
- الكود مقروء ونظيف: أي مبرمج Laravel سيفهم ما يفعله هذا السطر فورًا.
ومع ذلك، هذه هي الأعراض التي تبدأ بالظهور مع نمو التطبيق وزيادة الضغط عليه:
- زمن استجابة الخادم (TTFB) يزداد تدريجيًا.
- استهلاك الذاكرة (Memory Usage) لكل طلب يرتفع بشكل غير منطقي.
- عمليات PHP-FPM تصل إلى حدها الأقصى بسرعة، مما يؤدي إلى رفض طلبات جديدة (502 Bad Gateway).
ليش؟ شو القصة؟ ما دام الاستعلام سريع، من أين يأتي كل هذا البطء واستهلاك الموارد؟
الخطأ الخفي: “تضخم الكائنات” (Object Inflation)
المشكلة ليست في الاستعلام الذي أرسلته إلى MySQL. المشكلة تكمن في افتراض شائع ولكنه خاطئ جدًا:
“طالما أن استعلام قاعدة البيانات سريع، فإن التطبيق سيكون سريعًا.”
هذا الافتراض يتجاهل التكلفة الخفية لما يفعله Eloquent ORM. إطار العمل لا يتعامل مع البيانات التي تعود من قاعدة البيانات على أنها مجرد سطور نصية، بل يقوم بعملية معقدة تسمى “Hydration”، حيث يحوّل كل سطر إلى كائن PHP متكامل.
عندما تكتب Order::get()، أنت لا تجلب مجرد بيانات، بل أنت تقوم ببناء شبكة كاملة من الكائنات (Object Graph). كل موديل Order واحد هو كائن ثقيل يحمل بداخله:
- Attributes: مصفوفة للبيانات الأصلية.
- Casts: قواعد لتحويل أنواع البيانات (مثل تحويل النصوص إلى كائنات Carbon للتاريخ).
- Relations: تعريفات للعلاقات مع الموديلات الأخرى.
- Events: مستمعات للأحداث (retrieved, created, updated).
- Accessors & Mutators: دوال لتعديل البيانات عند جلبها أو حفظها.
- Metadata: معلومات داخلية يستخدمها Eloquent لتتبع حالة الكائن.
إذًا، في مثالنا، أنت لا تجلب 20 طلبًا و 20 مستخدمًا. أنت فعليًا تقوم ببناء 40 كائن Eloquent كامل، مع كل ما يرافقه من حمل زائد على الذاكرة والمعالج. كل كائن من هذه الكائنات هو “قنبلة موقوتة” من حيث استهلاك الموارد.
لماذا فشل الكاش في إنقاذ الموقف؟
الحل الأول الذي يقفز إلى ذهن أي مبرمج هو: “لنقم بتخزين هذه البيانات في الكاش!”. فيبدو الكود هكذا:
$orders = Cache::remember('dashboard_orders', 60, function () {
return Order::with('user')->latest()->take(20)->get();
});
وهنا تكمن الكارثة الثانية. ماذا يحدث فعليًا؟
- في الطلب الأول، يتم تنفيذ الاستعلام، بناء 40 كائن Eloquent، ثم يقوم Laravel بعملية Serialization لهذه الكائنات المعقدة وتخزينها في الكاش (سواء كان Redis أو ملفات).
- في كل طلب لاحق خلال مدة صلاحية الكاش، يقوم Laravel بجلب هذه البيانات من الكاش ثم يقوم بعملية Deserialization لإعادة بناء الـ 40 كائن Eloquent بكامل تعقيداتها في الذاكرة.
النتيجة؟ لقد تخلصت من استعلام قاعدة البيانات، نعم. لكنك استبدلته بعملية serialization/deserialization مكلفة جدًا على صعيد المعالج والذاكرة. أنت لم تحل المشكلة، بل قمت بنقلها من طبقة إلى أخرى، وفي بعض الحالات، جعلتها أسوأ.
الحل السحري (والغريب شوية): افصل نموذج العرض عن نموذج البيانات
إطار عمل Laravel مريح جدًا، لدرجة أنه يجعلنا ننسى التكلفة. والحل الحقيقي ليس في تحسين الاستعلام أو زيادة موارد السيرفر، بل في تغيير طريقة تفكيرنا.
الحل هو: فصل نموذج البيانات المخصص للعرض (Read Model) عن نموذج البيانات الخاص بالمنطق البرمجي (Write Model).
هذا يعني أنه في الأماكن الحساسة للأداء والتي تتطلب قراءة وعرض البيانات فقط (مثل لوحات التحكم، التقارير، واجهات برمجة التطبيقات العامة)، يجب أن نتجنب Eloquent تمامًا ونعود إلى الأساسيات.
الخطوة الأولى: استخدم Query Builder لجلب ما تحتاجه فقط
بدلاً من Eloquent، استخدم DB::table(). هذا يسمح لك ببناء استعلامات سريعة وخفيفة تجلب فقط الأعمدة التي تحتاجها للعرض، دون أي حمل زائد.
$ordersData = DB::table('orders')
->join('users', 'users.id', '=', 'orders.user_id')
->select([
'orders.id',
'orders.reference_number', // اسم عمود حقيقي
'orders.total',
'orders.created_at',
'users.name as user_name'
])
->orderByDesc('orders.created_at')
->limit(20)
->get();
الناتج هنا هو IlluminateSupportCollection من كائنات stdClass. كل كائن هو مجرد حاوية بسيطة للبيانات، بدون أي منطق أو metadata أو علاقات. خفيف جدًا على الذاكرة.
الخطوة الثانية: التحويل إلى بيانات خام جاهزة للعرض (اختياري ولكن موصى به)
لزيادة الكفاءة، يمكنك تحويل الكولكشن إلى مصفوفة بسيطة (plain array). هذا يضمن أن ما يتم تخزينه في الكاش هو أبسط شكل ممكن للبيانات.
$viewOrders = $ordersData->map(function ($order) {
return [
'id' => $order->id,
'reference' => $order->reference_number,
'total' => number_format($order->total / 100, 2), // مثال على تنسيق بسيط
'user' => $order->user_name,
'date' => $order->created_at, // يمكن تنسيقه هنا أو في الواجهة
];
})->all(); // ->all() لتحويل الكولكشن إلى مصفوفة
الخطوة الثالثة: التخزين المؤقت الذكي للبيانات النهائية
الآن، قم بتخزين هذه المصفوفة البسيطة في الكاش. عملية الـ serialization/deserialization لمصفوفة بسيطة أسرع بمرات لا تحصى من نفس العملية على كائنات Eloquent المعقدة.
$orders = Cache::remember('dashboard_orders_v2', 60, function () use ($ordersData) {
// يمكنك وضع منطق التحويل هنا مباشرةً
return $ordersData->map(fn ($o) => [
'id' => $o->id,
'reference' => $o->reference_number,
'total' => number_format($o->total / 100, 2),
'user' => $o->user_name,
'date' => $o->created_at,
])->all();
});
ماذا كسبت فعليًا؟
بتطبيق هذا النهج، أنت تخلصت من كل الحمل الزائد:
- لا يوجد بناء لكائنات Eloquent.
- لا توجد علاقات ليتم تحليلها.
- لا توجد Accessors أو Events ليتم تشغيلها.
- لا يوجد Metadata ليتم إدارتها.
ما لديك هو مجرد بيانات خام، جاهزة للعرض مباشرة. والنتيجة غالبًا ما تكون مذهلة:
- ⬇️ انخفاض في استهلاك الذاكرة بنسبة قد تصل إلى 40-60%.
- ⬇️ انخفاض ملحوظ في زمن استجابة الخادم (TTFB).
- ⬆️ زيادة هائلة في عدد الطلبات المتزامنة التي يمكن للخادم التعامل معها.
لماذا يقع في هذا الفخ حتى المبرمجون المحترفون؟
السبب بسيط: الراحة تتحول إلى عبء.
إطار عمل Laravel مصمم ليكون مريحًا وممتعًا في الاستخدام. Eloquent هو جوهرة التاج في هذا الإطار، فهو يخفي كل التعقيدات ويجعلك تشعر وكأنك “سوبرمان”. ولكن هذه الراحة لها ثمن، ومع مرور الوقت، ننسى أن نفكر في هذا الثمن.
نحن نعتاد على كتابة Model::with('relation')->get() في كل مكان، لأنها الطريقة “الصحيحة” والأنيقة. ويصبح هذا السلوك عادة متأصلة لدرجة أننا لا نتوقف لنسأل: “هل أنا حقًا بحاجة إلى كائن Eloquent كامل هنا، أم أنني فقط بحاجة إلى اسم المستخدم ورقم الطلب؟”.
الخلاصة: متى تقول “لأ” لـ Eloquent؟ 🏁
هذه المقالة ليست دعوة للتخلي عن Eloquent. على العكس تمامًا، Eloquent أداة عبقرية وقوية جدًا، ولكن يجب استخدامها في مكانها الصحيح.
نصيحتي العملية لك:
- استخدم Eloquent بكل قوته في منطق العمل (Business Logic): عند إنشاء سجل جديد، تحديثه، حذفه، تطبيق قواعد معقدة، تشغيل الأحداث… هنا يلمع Eloquent.
// مثالي لـ Eloquent $order = new Order($request->validated()); $order->user()->associate(auth()->user()); $order->recalculateTotals(); $order->save(); - تجنب Eloquent في طبقة العرض (Presentation Layer) الحساسة للأداء: في أي صفحة أو API endpoint يتوقع أن يستقبل عددًا كبيرًا من الزيارات ويعرض بيانات للقراءة فقط، فكر ألف مرة قبل استخدام
Model::get(). اسأل نفسك: هل أحتاج حقًا لكل هذا الثقل؟// استخدم Query Builder هنا $data = DB::table('...')->select([...])->get(); - لا تخزّن كائنات Eloquent في الكاش أبدًا. قم بتخزين مصفوفات بسيطة أو بيانات خام. هذه قاعدة ذهبية ستنقذ تطبيقك من مشاكل لا حصر لها.
تذكر دائمًا، Laravel أداة قوية، لكن القوة الحقيقية للمبرمج تكمن في معرفة متى يستخدم كل جزء من هذه الأداة، والأهم، متى لا يستخدمه.
ولو حسّيت إنك وقعت في المشكلة دي قبل كده، اطمن، أنت مش لوحدك، وأنت بالضبط الجمهور اللي كنت بكتبله هالكلام. 😉