إطار Laravel

أساسيات Eloquent ORM

20 دقيقة الدرس 7 من 45

أساسيات Eloquent ORM

Eloquent هو نظام التعيين العلائقي للكائنات (ORM) القوي والأنيق في Laravel. إنه يوفر تطبيقًا جميلًا وبسيطًا لـ ActiveRecord للعمل مع قاعدة البيانات الخاصة بك. كل جدول في قاعدة البيانات له "نموذج" مقابل يُستخدم للتفاعل مع هذا الجدول.

ما هو ORM؟

يتيح لك ORM (التعيين العلائقي للكائنات) التفاعل مع قاعدة البيانات باستخدام الكائنات والطرق بدلاً من كتابة استعلامات SQL الخام. يجعل Eloquent تفاعلات قاعدة البيانات تبدو طبيعية وبديهية.

نصيحة: مع Eloquent، تكتب أكود أقل وتحقق المزيد. بدلاً من كتابة استعلامات SQL يدويًا، تستخدم طرق PHP تعبيرية أسهل في القراءة والصيانة.

إنشاء أول نموذج لك

استخدم أمر Artisan لإنشاء نموذج جديد:

# إنشاء نموذج
php artisan make:model Product

# إنشاء نموذج مع هجرة
php artisan make:model Product -m

# إنشاء نموذج مع هجرة ومصنع وباذر
php artisan make:model Product -mfs

# إنشاء نموذج مع جميع الخيارات
php artisan make:model Product --all

ينشئ هذا ملف نموذج في app/Models/Product.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;
}

اصطلاحات التسمية في Eloquent

يتبع Eloquent اصطلاحات التسمية التي تقلل من التكوين. فهم هذه الاصطلاحات يساعدك على كتابة كود أنظف:

// اصطلاح اسم الجدول
// النموذج: Product → الجدول: products (جمع، أحرف صغيرة)
// النموذج: Category → الجدول: categories
// النموذج: OrderItem → الجدول: order_items (snake_case)

// اصطلاح المفتاح الأساسي
// افتراضي: عمود 'id' (عدد صحيح تلقائي التزايد)

// اصطلاح الطوابع الزمنية
// افتراضي: أعمدة created_at و updated_at

تخصيص الاصطلاحات

يمكنك تجاوز هذه الاصطلاحات عند الحاجة:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    // اسم جدول مخصص
    protected $table = 'my_products';

    // مفتاح أساسي مخصص
    protected $primaryKey = 'product_id';

    // مفتاح أساسي غير متزايد
    public $incrementing = false;

    // مفتاح أساسي غير عددي (مثل UUID)
    protected $keyType = 'string';

    // تعطيل الطوابع الزمنية
    public $timestamps = false;

    // أسماء أعمدة الطوابع الزمنية المخصصة
    const CREATED_AT = 'creation_date';
    const UPDATED_AT = 'updated_date';

    // اتصال قاعدة بيانات مخصص
    protected $connection = 'mysql2';
}

حماية التعيين الشامل

التعيين الشامل هو ميزة أمان تحمي من الثغرات. يجب عليك تحديد السمات التي يمكن تعيينها بشكل جماعي:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    // Fillable: نهج القائمة البيضاء (موصى به)
    protected $fillable = [
        'name',
        'description',
        'price',
        'quantity',
        'category_id',
    ];

    // Guarded: نهج القائمة السوداء
    // protected $guarded = ['id', 'created_at'];

    // السماح لجميع السمات (غير موصى به)
    // protected $guarded = [];
}
تحذير: لا تقم أبدًا بتعيين protected $guarded = []; دون فهم الآثار الأمنية. حدد دائمًا السمات القابلة للتعبئة بشكل صريح أو احمِ الحقول الحساسة بعناية.

عمليات CRUD مع Eloquent

إنشاء السجلات

<?php

use App\Models\Product;

// الطريقة 1: إنشاء وحفظ
$product = new Product;
$product->name = 'Laptop';
$product->description = 'High-performance laptop';
$product->price = 999.99;
$product->quantity = 50;
$product->save();

// الطريقة 2: التعيين الشامل مع create()
$product = Product::create([
    'name' => 'Mouse',
    'description' => 'Wireless mouse',
    'price' => 29.99,
    'quantity' => 100,
]);

// الطريقة 3: firstOrCreate() - البحث أو الإنشاء
$product = Product::firstOrCreate(
    ['name' => 'Keyboard'],
    ['price' => 79.99, 'quantity' => 30]
);

// الطريقة 4: updateOrCreate() - البحث والتحديث أو الإنشاء
$product = Product::updateOrCreate(
    ['name' => 'Monitor'],
    ['price' => 299.99, 'quantity' => 20]
);

قراءة السجلات

<?php

use App\Models\Product;

// استرجاع جميع السجلات
$products = Product::all();

// البحث بالمفتاح الأساسي
$product = Product::find(1);

// البحث أو الفشل (يرمي 404 إذا لم يُعثر عليه)
$product = Product::findOrFail(1);

// البحث بعمود آخر
$product = Product::where('name', 'Laptop')->first();

// الحصول على سجلات متعددة بشروط
$products = Product::where('price', '<', 100)->get();

// عد السجلات
$count = Product::where('quantity', '>', 0)->count();

// التحقق من وجود سجل
$exists = Product::where('name', 'Laptop')->exists();

// الحصول على القيم القصوى/الدنيا
$maxPrice = Product::max('price');
$minPrice = Product::min('price');

// الحصول على المجموع/المتوسط
$totalValue = Product::sum('price');
$avgPrice = Product::avg('price');

تحديث السجلات

<?php

use App\Models\Product;

// الطريقة 1: البحث والتحديث
$product = Product::find(1);
$product->price = 899.99;
$product->save();

// الطريقة 2: التحديث الشامل
Product::where('quantity', '<', 10)
    ->update(['status' => 'low_stock']);

// الطريقة 3: التحديث أو الإنشاء
$product = Product::updateOrCreate(
    ['name' => 'Tablet'],
    ['price' => 499.99, 'quantity' => 25]
);

// الزيادة/النقصان
$product = Product::find(1);
$product->increment('quantity');        // إضافة 1
$product->increment('quantity', 5);     // إضافة 5
$product->decrement('quantity', 3);     // طرح 3

// الزيادة مع تحديثات إضافية
$product->increment('views', 1, [
    'last_viewed_at' => now()
]);

حذف السجلات

<?php

use App\Models\Product;

// الطريقة 1: البحث والحذف
$product = Product::find(1);
$product->delete();

// الطريقة 2: الحذف بالمفتاح الأساسي
Product::destroy(1);
Product::destroy([1, 2, 3]);

// الطريقة 3: الحذف بشروط
Product::where('quantity', 0)->delete();

// الحذف القسري (تجاوز الحذف الناعم)
$product->forceDelete();

نطاقات الاستعلام

تتيح لك النطاقات تحديد قيود الاستعلام الشائعة التي يمكنك إعادة استخدامها بسهولة في جميع أنحاء تطبيقك.

النطاقات المحلية

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = ['name', 'price', 'quantity', 'is_active'];

    // نطاق: المنتجات النشطة
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    // نطاق: المنتجات في المخزون
    public function scopeInStock($query)
    {
        return $query->where('quantity', '>', 0);
    }

    // نطاق: المنتجات باهظة الثمن (مع معامل)
    public function scopeExpensive($query, $minPrice = 100)
    {
        return $query->where('price', '>=', $minPrice);
    }

    // نطاق: نطاق السعر
    public function scopePriceBetween($query, $min, $max)
    {
        return $query->whereBetween('price', [$min, $max]);
    }
}

استخدام النطاقات في استعلاماتك:

// استخدام نطاق واحد
$products = Product::active()->get();

// ربط نطاقات متعددة
$products = Product::active()
                   ->inStock()
                   ->expensive(200)
                   ->get();

// الجمع مع طرق الاستعلام الأخرى
$products = Product::active()
                   ->priceBetween(50, 200)
                   ->orderBy('name')
                   ->limit(10)
                   ->get();

الحذف الناعم

يسمح لك الحذف الناعم "بحذف" السجلات دون إزالتها فعليًا من قاعدة البيانات. يتم وضع علامة عليها بطابع زمني deleted_at.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use SoftDeletes;

    protected $fillable = ['name', 'price', 'quantity'];
}

// استخدام الحذف الناعم
$product = Product::find(1);
$product->delete();  // يضع طابع زمني deleted_at

// الاستعلامات تستبعد تلقائيًا السجلات المحذوفة ناعمًا
$products = Product::all();  // غير المحذوفة فقط

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

// الحصول فقط على السجلات المحذوفة ناعمًا
$products = Product::onlyTrashed()->get();

// استعادة سجل محذوف ناعمًا
$product = Product::withTrashed()->find(1);
$product->restore();

// الحذف القسري (دائم)
$product->forceDelete();
ملاحظة: لا تنسَ إضافة عمود deleted_at إلى هجرتك باستخدام $table->softDeletes();

الموصلات والمعدلات

تتيح لك الموصلات تنسيق قيم سمات Eloquent عند استردادها. تتيح لك المعدلات تنسيق القيم عند تعيينها.

الموصلات (Getters)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;

class Product extends Model
{
    // موصل للاسم (أحرف كبيرة تلقائيًا)
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => strtoupper($value),
        );
    }

    // موصل للسعر (منسق)
    protected function price(): Attribute
    {
        return Attribute::make(
            get: fn (float $value) => number_format($value, 2),
        );
    }

    // موصل افتراضي (سمة محسوبة)
    protected function fullDescription(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->name} - \${$this->price}",
        );
    }
}

// الاستخدام
$product = Product::find(1);
echo $product->name;              // LAPTOP
echo $product->price;             // 999.99
echo $product->full_description;  // LAPTOP - $999.99

المعدلات (Setters)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;

class Product extends Model
{
    // معدل للاسم (تخزين كأحرف صغيرة)
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
            set: fn (string $value) => strtolower($value),
        );
    }

    // معدل للسعر (التأكد من القيمة الموجبة)
    protected function price(): Attribute
    {
        return Attribute::make(
            set: fn (float $value) => max(0, $value),
        );
    }
}

// الاستخدام
$product = new Product;
$product->name = 'GAMING MOUSE';  // يُخزن كـ 'gaming mouse'
$product->price = -50;             // يُخزن كـ 0

تحويل السمات

تحويل السمات إلى أنواع بيانات شائعة تلقائيًا:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $casts = [
        'price' => 'decimal:2',
        'quantity' => 'integer',
        'is_active' => 'boolean',
        'options' => 'array',
        'specifications' => 'json',
        'published_at' => 'datetime',
        'discount_rate' => 'float',
    ];
}

// الاستخدام
$product = Product::find(1);

// تحويل منطقي
if ($product->is_active) {
    // يعمل مع 0/1، true/false، 'yes'/'no'
}

// تحويل المصفوفة (يُخزن كـ JSON)
$product->options = ['color' => 'red', 'size' => 'large'];
$product->save();

// استرجاع المصفوفة
$color = $product->options['color'];

// تحويل التاريخ والوقت
$published = $product->published_at;
echo $published->format('Y-m-d');
echo $published->diffForHumans();  // 'منذ يومين'
تمرين 1: إنشاء نموذج Post

قم بإنشاء نموذج Post بالميزات التالية:

  • السمات القابلة للتعبئة: title, slug, content, excerpt, is_published, published_at
  • الحذف الناعم ممكّن
  • تحويل is_published إلى boolean
  • تحويل published_at إلى datetime
  • إنشاء نطاق published() يصفي المنشورات المنشورة
  • إنشاء نطاق recent() يرتب حسب published_at تنازليًا
  • إنشاء موصل لـ excerpt يحده إلى 150 حرفًا
تمرين 2: تنفيذ نطاقات الاستعلام

أضف النطاقات التالية إلى نموذج المنتج الخاص بك:

  • scopeLowStock($query, $threshold = 10) - المنتجات ذات الكمية أقل من العتبة
  • scopeDiscount($query, $percentage) - المنتجات ذات الخصم >= النسبة المئوية
  • scopeCategory($query, $categoryId) - المنتجات في فئة محددة
  • scopeSearch($query, $term) - البحث في الاسم والوصف

ثم اكتب استعلامات تجمع نطاقات متعددة.

تمرين 3: ممارسة عمليات CRUD

قم بإنشاء نظام إدارة منتجات بسيط بهذه العمليات:

  • إنشاء 5 منتجات بأسعار وكميات مختلفة
  • البحث عن جميع المنتجات ذات السعر > 100
  • تحديث كمية منتج محدد
  • زيادة عدد المشاهدات لمنتج
  • حذف منتج ناعمًا
  • استعادة المنتج المحذوف ناعمًا
  • حذف منتج نهائيًا (حذف قسري)
  • حساب قيمة المخزون الإجمالية (مجموع السعر * الكمية)

أفضل الممارسات

  • استخدم دائمًا حماية التعيين الشامل: حدد $fillable أو $guarded لمنع الثغرات الأمنية.
  • استخدم نطاقات الاستعلام: قم بتغليف الاستعلامات الشائعة في نطاقات لإعادة الاستخدام والقراءة.
  • استفد من الحذف الناعم: استخدم الحذف الناعم للسجلات التي قد تحتاج إلى استرداد.
  • تحويل السمات: استخدم تحويل السمات لضمان صحة أنواع البيانات واتساقها.
  • استخدم الموصلات بحكمة: لا تقم بإجراء حسابات ثقيلة في الموصلات؛ إنها تعمل في كل مرة يتم فيها الوصول إلى السمة.
  • اتبع اصطلاحات التسمية: التزم باصطلاحات Laravel لكتابة كود تكوين أقل.
  • استخدم findOrFail(): في وحدات التحكم، استخدم findOrFail() لإرجاع 404 تلقائيًا للسجلات المفقودة.

الخلاصة

في هذا الدرس، أتقنت:

  • إنشاء وتكوين نماذج Eloquent
  • فهم اصطلاحات التسمية في Eloquent
  • تنفيذ حماية التعيين الشامل
  • إجراء عمليات CRUD بصيغة أنيقة
  • إنشاء واستخدام نطاقات الاستعلام
  • تنفيذ الحذف الناعم لإزالة البيانات بأمان
  • استخدام الموصلات والمعدلات لتنسيق البيانات
  • تحويل السمات إلى أنواع البيانات المناسبة

يجعل Eloquent عمليات قاعدة البيانات بديهية وتعبيرية. مع إتقان هذه الأساسيات، أنت مستعد لاستكشاف العلاقات في الدرس التالي!