أساسيات Eloquent ORM في Laravel
Eloquent هو "مترجم قاعدة البيانات" في Laravel — تتفاعل معه باستخدام كائنات PHP، وهو يترجمها إلى استعلامات SQL للتنفيذ.
1. ما ستتعلمه
- إنشاء النموذج وتعريف الخصائص: $fillable/$guarded/$casts/$attributes
- سير عمل CRUD الكامل: create/all/find/update/delete والتعيين المجمّع
- باني الاستعلامات: where/orderBy/groupBy/استعلامات فرعية
- عمليات المجموعات: filter/map/reduce/each المعالجة المتسلسلة
- الحذف الناعم والاستعادة: سمة SoftDeletes
2. قصة حقيقية لمطور متكامل
(1) المشكلة: ربط سلاسل SQL يؤدي إلى حقن وفقدان بيانات
في الأيام الأولى، كتب Bob تطبيق ShopMetrics بلغة PHP أصلية — جميع استعلامات SQL كانت تُبنى باستخدام ربط السلاسل: "SELECT * FROM shops WHERE id = " . $_GET['id']. قام مخترق بحقن 1 OR 1=1 في معرف متجر Alice، مما تسبب في تسريب بيانات الموقع بالكامل. مشكلة أكثر شيوعًا كانت عندما نسي Bob تضمين عبارة WHERE في استعلام UPDATE؛ أمر واحد أعاد إيرادات جميع المتاجر إلى صفر، واستغرق استعادة البيانات يومًا كاملًا.
(2) حل Eloquent ORM
يستخدم Eloquent كائنات PHP للتفاعل مع قواعد البيانات، ويوفر ربط معلمات تلقائي لمنع هجمات الحقن، ويحمي الحقول الحساسة بالتعيين المجمّع، ويستخدم الحذف الناعم لمنع الحذف العرضي.
// آمن، مقروء، لا يمكن حدوث حقن SQL
$shop = Shop::create([
'name' => 'Alice Store',
'tenant_id' => 1,
]);
// حماية التعيين المجمّع — فقط حقول $fillable مسموح بها
protected $fillable = ['name', 'slug', 'tenant_id'];
// revenue ليست في $fillable — لا يمكن تعيينها عبر create()
(3) العائد
بعد أن بدأ Bob استخدام Eloquent، أُزيل خطر حقن SQL، ويمكن استعادة البيانات المحذوفة عرضيًا بنقرة واحدة باستخدام الحذف الناعم. انخفض حجم الكود من 200 سطر SQL إلى 30 سطر PHP.
3. تعريف النموذج
(1) إنشاء نموذج
php artisan make:model Shop
# ينشئ: app/Models/Shop.php
# مع تهجير
php artisan make:model Shop -m
# ينشئ: app/Models/Shop.php + database/migrations/create_shops_table.php
(2) تهيئة خصائص النموذج
// app/Models/Shop.php
class Shop extends Model
{
protected $fillable = [
'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
];
protected $guarded = ['id']; // بديل: حظر حقول محددة
protected $attributes = [
'status' => 'active',
'revenue' => 0,
];
protected $casts = [
'revenue' => 'decimal:2',
'is_active' => 'boolean',
'metadata' => 'json',
'launched_at' => 'datetime',
];
}
| الخاصية | الوظيفة | الطريقة الموصى بها |
|---|---|---|
$fillable |
حقول تسمح بالتعيين المجمّع | ✅ قائمة بيضاء |
$guarded |
حقول يُحظر التعيين المجمّر لها | ❌ قائمة سوداء |
$casts |
تحويل نوع تلقائي | مطلوب |
$attributes |
قيمة افتراضية للحقل | بديل للقيمة الافتراضية في DB |
(3) مخطط أصناف نموذج Eloquent
classDiagram
class Model {
+save()
+delete()
+update(array data)
+fresh()
+refresh()
+toArray()
+toJson()
}
class Shop {
+array fillable
+array casts
+tenant()
+orders()
+products()
}
class SoftDeletes {
+forceDelete()
+restore()
+trashed()
+withTrashed()
+onlyTrashed()
}
Model <|-- Shop
Shop ..|> SoftDeletes : uses trait
(1) ▶ مثال: نموذج متجر ShopMetrics
// app/Models/Shop.php
class Shop extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
];
protected $casts = [
'revenue' => 'decimal:2',
'metadata' => 'array',
];
protected $attributes = [
'status' => 'active',
'revenue' => 0,
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function scopeActive(Builder $query): Builder
{
return $query->where('status', 'active');
}
}
الناتج:
// تم التنفيذ بنجاح
4. عمليات CRUD
(1) إنشاء
// الطريقة 1: create() مع التعيين المجمّع
$shop = Shop::create([
'tenant_id' => 1,
'name' => 'Alice Store',
'slug' => 'alice-store',
]);
// الطريقة 2: new + save
$shop = new Shop();
$shop->tenant_id = 1;
$shop->name = 'Alice Store';
$shop->slug = 'alice-store';
$shop->save();
// الطريقة 3: firstOrCreate — ابحث أو أنشئ
$shop = Shop::firstOrCreate(
['slug' => 'alice-store'], // معايير البحث
['name' => 'Alice Store', 'tenant_id' => 1], // القيم عند الإنشاء
);
// الطريقة 4: updateOrCreate — حدّث أو أنشئ
$shop = Shop::updateOrCreate(
['slug' => 'alice-store'],
['name' => 'Alice Store Updated', 'revenue' => 5000],
);
(2) قراءة
// البحث بالمفتاح الأساسي
$shop = Shop::find(1);
$shop = Shop::findOrFail(1); // يرمي 404 إذا لم يُوجد
// البحث بعمود
$shop = Shop::where('slug', 'alice-store')->first();
$shop = Shop::whereSlug('alice-store')->firstOrFail();
// جلب الكل
$shops = Shop::all();
$shops = Shop::active()->get(); // باستخدام نطاق
// التقسيم لمجموعات البيانات الكبيرة
Shop::chunk(200, function ($shops) {
foreach ($shops as $shop) {
// معالجة 200 متجر في كل مرة
}
});
(3) تحديث
// تحديث نموذج واحد
$shop->update(['name' => 'New Name']);
// تحديث عبر استعلام
Shop::where('status', 'suspended')->update(['status' => 'active']);
// زيادة/إنقاص
$shop->increment('revenue', 1500);
Shop::whereId(1)->decrement('stock', 5);
(4) حذف
// حذف ناعم (يعيّن deleted_at)
$shop->delete();
// حذف قسري (دائم)
$shop->forceDelete();
// استعادة المحذوف ناعمًا
$shop->restore();
// استعلام مع المحذوفات
Shop::withTrashed()->where('id', 1)->first();
Shop::onlyTrashed()->get();
(1) ▶ مثال: سير عمل CRUD كامل لـ ShopMetrics
// إنشاء متجر مع منتجات
$shop = Shop::create([
'tenant_id' => 1,
'name' => 'Bob Electronics',
'slug' => 'bob-electronics',
]);
$shop->products()->createMany([
['name' => 'Widget A', 'sku' => 'W-001', 'price' => 29.99],
['name' => 'Widget B', 'sku' => 'W-002', 'price' => 49.99],
]);
// قراءة مع التحميل المسبق
$shop = Shop::with('products')->whereSlug('bob-electronics')->firstOrFail();
// تحديث المتجر والمنتج
$shop->update(['revenue' => 15000]);
$shop->products()->whereSku('W-001')->update(['price' => 34.99]);
// حذف ناعم واستعادة
$shop->delete();
Shop::withTrashed()->whereSlug('bob-electronics')->first()->restore();
الناتج:
// تم التنفيذ بنجاح
5. باني الاستعلامات
(1) استعلام شرطي
$shops = Shop::where('status', 'active')
->where('revenue', '>', 1000)
->orWhere(function ($query) {
$query->where('status', 'new')
->where('created_at', '>', now()->subDays(7));
})
->get();
// where ديناميكي
$shops = Shop::whereStatus('active')
->whereRevenueGreaterThan(1000)
->get();
(2) الترتيب والتجميع والتقسيم
// ترتيب
$shops = Shop::orderBy('revenue', 'desc')->get();
// تجميع مع having
$revenueByStatus = Shop::select('status', DB::raw('SUM(revenue) as total'))
->groupBy('status')
->having('total', '>', 1000)
->get();
// تقسيم الصفحات
$shops = Shop::where('tenant_id', 1)->paginate(15);
$shops = Shop::where('tenant_id', 1)->simplePaginate(15);
$shops = Shop::where('tenant_id', 1)->cursorPaginate(15);
| طريقة التقسيم | تنفيذ الاستعلام | حالات الاستخدام |
|---|---|---|
paginate() |
COUNT + SELECT | مطلوب عدد الصفحات الإجمالي |
simplePaginate() |
SELECT فقط | غير مطلوب عدد الصفحات الإجمالي |
cursorPaginate() |
SELECT مع WHERE فقط | الأكثر كفاءة لمجموعات البيانات الكبيرة |
(3) استعلامات فرعية
// استعلام فرعي في select
$shops = Shop::select('shops.*')
->selectSub(
Order::selectRaw('SUM(total)')
->whereColumn('shop_id', 'shops.id'),
'orders_total'
)
->get();
// استعلام فرعي في where
$latestOrders = Shop::where('created_at', function ($query) {
$query->selectRaw('MAX(created_at)')
->from('orders')
->whereColumn('shop_id', 'shops.id');
})->get();
(1) ▶ مثال: استعلام معقد لـ ShopMetrics
// أفضل 10 متاجر حسب الإيرادات في المستأجر الحالي، مع عدد الطلبات
$topShops = Shop::select('shops.*')
->selectSub(
Order::selectRaw('COUNT(*)')
->whereColumn('shop_id', 'shops.id')
->where('created_at', '>=', now()->subDays(30)),
'recent_orders_count'
)
->where('tenant_id', tenant()->id)
->where('status', 'active')
->orderBy('revenue', 'desc')
->take(10)
->get();
الناتج:
// تم التنفيذ بنجاح
6. عمليات المجموعات
دالة get() في Eloquent تعيد كائن Collection، الذي يوفر دوال متسلسلة أقوى من المصفوفات.
| الطريقة | الوظيفة | مكافئ SQL |
|---|---|---|
filter() |
تصفية | WHERE |
map() |
تحويل تعييني | تحويل SELECT |
sortBy() |
ترتيب | ORDER BY |
groupBy() |
تجميع | GROUP BY |
sum() |
جمع | SUM() |
count() |
عد | COUNT() |
pluck() |
استخراج عمود | SELECT عمود واحد |
unique() |
إزالة التكرارات | DISTINCT |
each() |
تكرار وتنفيذ | — |
reduce() |
حساب تراكمي | — |
(1) ▶ مثال: عمليات سلسلة مجموعات ShopMetrics
// جلب جميع المتاجر لمستأجر، تصفية وتحويل
$topShops = Shop::where('tenant_id', 1)
->with('products')
->get()
->filter(fn ($shop) => $shop->revenue > 1000)
->sortByDesc('revenue')
->map(fn ($shop) => [
'name' => $shop->name,
'revenue' => $shop->revenue,
'product_count' => $shop->products->count(),
])
->take(10);
// تجميع المتاجر حسب الحالة وعدّها
$shopsByStatus = Shop::where('tenant_id', 1)
->get()
->groupBy('status')
->map(fn ($group) => $group->count());
// ['active' => 15, 'suspended' => 2, 'closed' => 1]
// استخراج المعرفات للعمليات المجمعة
$shopIds = Shop::where('status', 'active')->pluck('id');
// [1, 2, 5, 8, 12]
الناتج:
// تم التنفيذ بنجاح
7. الحذف الناعم
(1) تفعيل الحذف الناعم
// النموذج
class Shop extends Model
{
use SoftDeletes;
protected $casts = [
'deleted_at' => 'datetime',
];
}
// التهجير
$table->softDeletes(); // يضيف deleted_at TIMESTAMP NULL
(2) عمليات الحذف الناعم
// حذف (ناعم — يعيّن deleted_at)
$shop->delete();
// التحقق من الحذف الناعم
$shop->trashed(); // true إذا حُذف ناعمًا
// تضمين السجلات المحذوفة ناعمًا
Shop::withTrashed()->get();
// فقط السجلات المحذوفة ناعمًا
Shop::onlyTrashed()->get();
// استعادة
$shop->restore();
// حذف دائم
$shop->forceDelete();
(1) ▶ مثال: سيناريو استعادة الحذف الناعم في ShopMetrics
// حذف Alice متجرًا بالخطأ
$shop = Shop::whereSlug('alice-store')->first();
$shop->delete();
// Bob لا يزال يجده في السجلات المحذوفة
$trashed = Shop::onlyTrashed()->whereSlug('alice-store')->first();
// استعادة المتجر مع جميع العلاقات سليمة
if ($trashed) {
$trashed->restore();
// $trashed->products لا تزال موجودة — لم تُحذف
}
الناتج:
// تم التنفيذ بنجاح
8. مثال شامل: تحليل طلبات ShopMetrics
// ============================================
// شامل: تحليلات طلبات ShopMetrics
// يغطي: CRUD، الاستعلامات، المجموعات، الحذف الناعم، النطاقات
// ============================================
// app/Models/Order.php
class Order extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id', 'shop_id', 'user_id', 'order_number',
'subtotal', 'discount', 'total', 'status', 'metadata',
];
protected $casts = [
'total' => 'decimal:2',
'metadata' => 'array',
'deleted_at' => 'datetime',
];
public function shop(): BelongsTo
{
return $this->belongsTo(Shop::class);
}
public function scopeCompleted(Builder $query): Builder
{
return $query->where('status', 'completed');
}
public function scopeThisMonth(Builder $query): Builder
{
return $query->whereBetween('created_at', [
now()->startOfMonth(), now()->endOfMonth(),
]);
}
}
// استعلام التحليلات — تقرير الإيرادات الشهري
$monthlyReport = Order::where('tenant_id', tenant()->id)
->completed()
->thisMonth()
->with('shop')
->get()
->groupBy('shop.name')
->map(fn ($orders) => [
'shop' => $orders->first()->shop->name,
'order_count' => $orders->count(),
'revenue' => $orders->sum('total'),
'avg_order' => $orders->avg('total'),
])
->sortByDesc('revenue')
->values();
❓ أسئلة شائعة
findOrFail؟findOrFail عندما تحتاج لإرجاع صفحة 404 إذا لم تُوجد سجلات. إذا عدم إيجاد سجلات هو جزء من المنطق التجاري الطبيعي (مثل بحث بلا نتائج)، استخدم find وتحقق من null.WHERE في SQL)، بينما المجموعات تعمل في الذاكرة (مثل التصفية بـ filter في PHP). مجموعات البيانات الكبيرة يجب تصفيتها على مستوى الاستعلام، بينما مجموعات النتائج الصغيرة يمكنها استخدام دوال المجموعات.deleted_at؛ البيانات المرتبطة تبقى في قاعدة البيانات. بمجرد استعادة النموذج الأب، تصبح العلاقة متاحة فورًا. إذا كنت بحاجة لإجراء حذف ناعم متسلسل، يمكنك الاستماع لحدث deleting في دالة boot() للنموذج.DB::table()) عندما تحتاج فقط نتائج استعلام بسيطة ولا تحتاج نماذج. Eloquent هو في جوهره غلاف حول Query Builder.Shop::where(...)->update([...]) ينفذ عبارة SQL واحدة لتحديث جميع السجلات المطابقة؛ هذا فعال للغاية. $shops->each->update([...]) ينفذ عبارات SQL واحدة تلو الأخرى، مما يطلق أحداث النموذج. استخدم التحديث الفردي عندما تحتاج لإطلاق الأحداث.📖 ملخص
- يستخدم Eloquent كائنات PHP للتفاعل مع قواعد البيانات، مع ربط معلمات تلقائي لمنع حقن SQL
- القوائم البيضاء $fillable تحمي من التعيين المجمّر؛ $casts يقوم بتحويل نوع تلقائي
- الخطوات الأربع لـ CRUD: create/read/update/delete؛
findOrFailيعيد 404 - باني الاستعلامات يدعم WHERE وORDER BY وGROUP BY والاستعلامات الفرعية
- المجموعات توفر عمليات متسلسلة أقوى من المصفوفات (filter/map/sortBy)
- الحذف الناعم يُعلَّم بـ
deleted_atبدلاً من الحذف الدائم؛ يدعم الاستعادة
📝 تمارين
-
تمرين أساسي (⭐): أنشئ نموذج
Productلـ ShopMetrics، حدد$fillableو$casts، نفذ عمليات CRUD كاملة (إنشاء، قراءة، تحديث، حذف)، واستخدم Tinker للتحقق من كل عملية. -
سؤال متقدم (⭐⭐): اكتب استعلامًا لاسترداد أفضل 5 متاجر ذات أعلى إيرادات تحت المستأجر الحالي، مع عدد طلباتها. استخدم الاستعلام الفرعي
selectSubودالةmapفيCollectionلتنسيق الإخراج. -
تحدٍ (⭐⭐⭐): نفذ حذفًا ناعمًا واستعادة متسلسلة لنموذج Order: عند حذف Order، يُحذف OrderItems ناعمًا أيضًا؛ عند استعادة Order، يُستعاد OrderItems معها. نفذ ذلك باستخدام مستمعي أحداث النموذج.



