📍 الدليل المرجعي: Laravel 11 وPHP 8.4: تثبيت ومعمارية
Eloquent هو ORM لـ Laravel — يجسر بين كائنات PHP وجداول SQL. في Laravel 11، عدّة ميزات وُحِّدت أو بُسِّطت: السمات #[ScopedBy]، trait HasUuids، الدالّة withAttributes()، وأمر php artisan model:show. يُغطّي هذا الدليل العلاقات وscopes وcasting وأنماط الاستعلام المتقدّمة.
المتطلّبات
- مشروع Laravel 11 مُهَيَّأ.
- قاعدة بيانات متّصلة.
- أساسيات SQL.
- الوقت: 60 دقيقة.
الخطوة 1 — إنشاء نموذج Eloquent مع كلّ تبعيّاته
# Article + migration + factory + seeder + policy + controller
php artisan make:model Article -a
# فحص النموذج
php artisan model:show Article
الأمر model:show من جديد Laravel 11: يعرض في الـ terminal أعمدة الجدول، أنواعها، $fillable، casts، العلاقات، وscopes.
الخطوة 2 — تهيئة fillable وcasts وUUID
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Article extends Model
{
use HasUuids, SoftDeletes;
protected $fillable = [
'title', 'content', 'slug', 'status', 'published_at', 'user_id',
];
protected $casts = [
'published_at' => 'datetime:Y-m-d',
'status' => ArticleStatus::class,
'metadata' => 'array',
];
protected $hidden = ['deleted_at'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
trait HasUuids يفعل: يُعَرِّف المفتاح غير قابل للزيادة، يُوَلِّد UUID مُرَتَّبًا، ويُكَيِّف migrations لقبول UUID. في migration، استبدل $table->id() بـ $table->uuid('id')->primary().
الخطوة 3 — العمل مع علاقات Eloquent
Eloquent يُدير: hasOne، hasMany، belongsTo، belongsToMany، hasManyThrough، morphTo. مفتاح الكفاءة فهم متى نُحَمِّل في eager loading (with()) مقابل lazy. Lazy يُولِّد N+1: لـ 50 مقالًا مع مؤلّفها، 51 استعلام SQL.
<?php
// مشكلة N+1 — تجنّبها
$articles = Article::all();
foreach ($articles as $article) {
echo $article->user->name;
}
// Eager loading — صحيح: استعلامان
$articles = Article::with('user')->get();
foreach ($articles as $article) {
echo $article->user->name;
}
// Eager loading مشروط
$articles = Article::with(['comments' => function ($query) {
$query->where('approved', true)->latest()->limit(3);
}])->get();
لاكتشاف N+1 في التطوير، فعّل الكشف التلقائي في AppServiceProvider:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
الخطوة 4 — Scopes محلّية وشاملة
<?php
// scopes محلّية
public function scopePublished(Illuminate\Database\Eloquent\Builder $query): void
{
$query->where('status', 'published')
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
public function scopeByAuthor(Illuminate\Database\Eloquent\Builder $query, int $userId): void
{
$query->where('user_id', $userId);
}
// الاستعمال
$recentPublished = Article::published()
->byAuthor(auth()->id())
->with('user')
->latest('published_at')
->paginate(10);
لـ scopes شاملة، Laravel 11 يُدخل سمة #[ScopedBy] التي تحلّ محلّ boot():
<?php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('status', 'published');
}
}
// app/Models/Article.php
use App\Models\Scopes\PublishedScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
#[ScopedBy([PublishedScope::class])]
class Article extends Model
{
// كلّ استعلامات Article تضمّ تلقائيًّا WHERE status = 'published'
}
لتجاهل scope شامل: Article::withoutGlobalScope(PublishedScope::class)->get().
الخطوة 5 — تحسين الاستعلامات: select، chunk، cursor، lazy
<?php
// اختيار الأعمدة اللازمة
$articles = Article::select('id', 'title', 'slug', 'published_at')->published()->get();
// chunk() — معالجة بدفعات
Article::published()->chunk(500, function ($articles) {
foreach ($articles as $article) {
dispatch(new GenerateArticlePdf($article));
}
});
// lazy() — generator Eloquent
foreach (Article::published()->lazy() as $article) {
$article->update(['view_count' => 0]);
}
// cursor() — generator منخفض المستوى عبر PDO
foreach (Article::published()->cursor() as $article) {
echo $article->title . PHP_EOL;
}
قاعدة: get() لمجموعات صغيرة، chunk() لمعالجات ضخمة مع dispatch، lazy() حين تحتاج كامل ميزات Eloquent بذاكرة قليلة، وcursor() لسكريبتات CLI تقرأ فقط.
الخطوة 6 — Soft Deletes وأمر model:prune
<?php
// Migration
$table->softDeletes();
// النموذج
use Illuminate\Database\Eloquent\SoftDeletes;
class Article extends Model
{
use SoftDeletes;
}
// العمليات
$article->delete();
Article::withTrashed()->get();
Article::onlyTrashed()->get();
$article->restore();
$article->forceDelete();
<?php
use Illuminate\Database\Eloquent\Prunable;
class Article extends Model
{
use SoftDeletes, Prunable;
public function prunable(): Illuminate\Database\Eloquent\Builder
{
return static::where('deleted_at', '<=', now()->subDays(30));
}
}
// routes/console.php
Schedule::command('model:prune')->daily();
أخطاء شائعة
| الخطأ | السبب | الحلّ |
|---|---|---|
| LazyLoadingViolationException | وصول لعلاقة دون eager loading | أضف with('relation') |
| UUIDs لا تُوَلَّد | $table->id() في migration |
صَحِّح إلى $table->uuid('id')->primary() |
| soft-deleted تظهر | نسيان trait SoftDeletes |
أضف use SoftDeletes; |
| scope شامل غير مُطَبَّق | boot() بدل #[ScopedBy] |
هاجر إلى #[ScopedBy([MyScope::class])] |
FAQ
الفرق بين $fillable و$guarded؟ $fillable قائمة بيضاء. $guarded قائمة سوداء. $guarded = [] أسرع لكن أقلّ أمنًا.
HasUuids v4 أم v7؟ في Laravel 11، HasUuids يُوَلِّد UUID مُرَتَّبًا v4 ببادئة زمنية. v7 أصلي متاح منذ Laravel 12 عبر HasVersion7Uuids، أنسب لفهارس قواعد البيانات.
Enum PHP كـ casts؟ نعم. منذ Laravel 9 وPHP 8.1: 'status' => ArticleStatus::class.