404 Not Found

404 Not Found


nginx

أساسيات Eloquent ORM في Laravel

Eloquent هو "مترجم قاعدة البيانات" في Laravel — تتفاعل معه باستخدام كائنات PHP، وهو يترجمها إلى استعلامات SQL للتنفيذ.

1. ما ستتعلمه


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 للتفاعل مع قواعد البيانات، ويوفر ربط معلمات تلقائي لمنع هجمات الحقن، ويحمي الحقول الحساسة بالتعيين المجمّع، ويستخدم الحذف الناعم لمنع الحذف العرضي.

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) إنشاء نموذج

BASH
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) تهيئة خصائص النموذج

PHP
// 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

100%
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

PHP
// 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');
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

4. عمليات CRUD

(1) إنشاء

PHP
// الطريقة 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) قراءة

PHP
// البحث بالمفتاح الأساسي
$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) تحديث

PHP
// تحديث نموذج واحد
$shop->update(['name' => 'New Name']);

// تحديث عبر استعلام
Shop::where('status', 'suspended')->update(['status' => 'active']);

// زيادة/إنقاص
$shop->increment('revenue', 1500);
Shop::whereId(1)->decrement('stock', 5);

(4) حذف

PHP
// حذف ناعم (يعيّن deleted_at)
$shop->delete();

// حذف قسري (دائم)
$shop->forceDelete();

// استعادة المحذوف ناعمًا
$shop->restore();

// استعلام مع المحذوفات
Shop::withTrashed()->where('id', 1)->first();
Shop::onlyTrashed()->get();

(1) ▶ مثال: سير عمل CRUD كامل لـ ShopMetrics

PHP
// إنشاء متجر مع منتجات
$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();

الناتج:

TEXT
// تم التنفيذ بنجاح

5. باني الاستعلامات

(1) استعلام شرطي

PHP
$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) الترتيب والتجميع والتقسيم

PHP
// ترتيب
$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) استعلامات فرعية

PHP
// استعلام فرعي في 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

PHP
// أفضل 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();

الناتج:

TEXT
// تم التنفيذ بنجاح

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

PHP
// جلب جميع المتاجر لمستأجر، تصفية وتحويل
$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]

الناتج:

TEXT
// تم التنفيذ بنجاح

7. الحذف الناعم

(1) تفعيل الحذف الناعم

PHP
// النموذج
class Shop extends Model
{
    use SoftDeletes;

    protected $casts = [
        'deleted_at' => 'datetime',
    ];
}

// التهجير
$table->softDeletes(); // يضيف deleted_at TIMESTAMP NULL

(2) عمليات الحذف الناعم

PHP
// حذف (ناعم — يعيّن deleted_at)
$shop->delete();

// التحقق من الحذف الناعم
$shop->trashed(); // true إذا حُذف ناعمًا

// تضمين السجلات المحذوفة ناعمًا
Shop::withTrashed()->get();

// فقط السجلات المحذوفة ناعمًا
Shop::onlyTrashed()->get();

// استعادة
$shop->restore();

// حذف دائم
$shop->forceDelete();

(1) ▶ مثال: سيناريو استعادة الحذف الناعم في ShopMetrics

PHP
// حذف Alice متجرًا بالخطأ
$shop = Shop::whereSlug('alice-store')->first();
$shop->delete();

// Bob لا يزال يجده في السجلات المحذوفة
$trashed = Shop::onlyTrashed()->whereSlug('alice-store')->first();

// استعادة المتجر مع جميع العلاقات سليمة
if ($trashed) {
    $trashed->restore();
    // $trashed->products لا تزال موجودة — لم تُحذف
}

الناتج:

TEXT
// تم التنفيذ بنجاح

8. مثال شامل: تحليل طلبات ShopMetrics

PHP
// ============================================
// شامل: تحليلات طلبات 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();

❓ أسئلة شائعة

س ما الفرق بين $fillable و$guarded؟
ج $fillable هي قائمة بيضاء (فقط هذه الحقول مسموح بتعيينها بشكل مجمّع)، بينما $guarded هي قائمة سوداء (هذه الحقول محظورة). نوصي باستخدام القائمة البيضاء $fillable، فهي أكثر أمانًا — يجب عليك الإعلان صراحةً عن الحقول المسموح بتعيينها.
س متى يجب استخدام findOrFail؟
ج استخدم findOrFail عندما تحتاج لإرجاع صفحة 404 إذا لم تُوجد سجلات. إذا عدم إيجاد سجلات هو جزء من المنطق التجاري الطبيعي (مثل بحث بلا نتائج)، استخدم find وتحقق من null.
س ما الفرق بين دوال المجموعات ودوال باني الاستعلامات؟
ج باني الاستعلامات يعمل على مستوى قاعدة البيانات (مثل التصفية بـ WHERE في SQL)، بينما المجموعات تعمل في الذاكرة (مثل التصفية بـ filter في PHP). مجموعات البيانات الكبيرة يجب تصفيتها على مستوى الاستعلام، بينما مجموعات النتائج الصغيرة يمكنها استخدام دوال المجموعات.
س ماذا يحدث لعلاقات النموذج بعد الحذف الناعم؟
ج الحذف الناعم يعيّن فقط حقل deleted_at؛ البيانات المرتبطة تبقى في قاعدة البيانات. بمجرد استعادة النموذج الأب، تصبح العلاقة متاحة فورًا. إذا كنت بحاجة لإجراء حذف ناعم متسلسل، يمكنك الاستماع لحدث deleting في دالة boot() للنموذج.
س كيف أختار بين Eloquent وQuery Builder؟
ج استخدم Eloquent عندما تحتاج نسخ نماذج (للوصول إلى الارتباطات أو استخدام السمات)؛ استخدم Query Builder (DB::table()) عندما تحتاج فقط نتائج استعلام بسيطة ولا تحتاج نماذج. Eloquent هو في جوهره غلاف حول Query Builder.
س ما الفرق بين التحديث المجمّع والتحديث الفردي؟
ج Shop::where(...)->update([...]) ينفذ عبارة SQL واحدة لتحديث جميع السجلات المطابقة؛ هذا فعال للغاية. $shops->each->update([...]) ينفذ عبارات SQL واحدة تلو الأخرى، مما يطلق أحداث النموذج. استخدم التحديث الفردي عندما تحتاج لإطلاق الأحداث.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ نموذج Product لـ ShopMetrics، حدد $fillable و$casts، نفذ عمليات CRUD كاملة (إنشاء، قراءة، تحديث، حذف)، واستخدم Tinker للتحقق من كل عملية.

  2. سؤال متقدم (⭐⭐): اكتب استعلامًا لاسترداد أفضل 5 متاجر ذات أعلى إيرادات تحت المستأجر الحالي، مع عدد طلباتها. استخدم الاستعلام الفرعي selectSub ودالة map في Collection لتنسيق الإخراج.

  3. تحدٍ (⭐⭐⭐): نفذ حذفًا ناعمًا واستعادة متسلسلة لنموذج Order: عند حذف Order، يُحذف OrderItems ناعمًا أيضًا؛ عند استعادة Order، يُستعاد OrderItems معها. نفذ ذلك باستخدام مستمعي أحداث النموذج.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%