Eloquent المتقدم: نطاقات الاستعلام والبناة
مقدمة إلى تقنيات الاستعلام المتقدمة
مع نمو تطبيقات Laravel الخاصة بك، ستحتاج إلى طرق أكثر تطوراً للتعامل مع استعلامات قاعدة البيانات. نطاقات الاستعلام والبناة المخصصة تساعدك على كتابة منطق قاعدة بيانات قابل لإعادة الاستخدام ومعبّر وسهل الصيانة.
- تغليف منطق الاستعلام المعقد في طرق قابلة لإعادة الاستخدام
- الحفاظ على Controllers والخدمات نظيفة ومركزة
- ربط النطاقات للحصول على استعلامات مرنة وقابلة للقراءة
- تقليل تكرار الكود في تطبيقك
النطاقات المحلية (Local Scopes)
النطاقات المحلية هي طرق تعرّفها على نماذج Eloquent الخاصة بك لتغليف منطق الاستعلام. تبدأ دائماً بكلمة "scope" متبوعة باسم الطريقة بصيغة CamelCase.
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
// نطاق للحصول على المنشورات المنشورة فقط
public function scopePublished($query)
{
return $query->where('status', 'published');
}
// نطاق للحصول على المنشورات من آخر N يوم
public function scopeRecent($query, $days = 7)
{
return $query->where('created_at', '>=', now()->subDays($days));
}
// نطاق للتصفية حسب الفئة
public function scopeOfCategory($query, $category)
{
return $query->where('category', $category);
}
// نطاق مع شروط متعددة
public function scopeFeatured($query)
{
return $query->where('is_featured', true)
->where('status', 'published')
->orderBy('views', 'desc');
}
}
// الحصول على المنشورات المنشورة
$publishedPosts = Post::published()->get();
// ربط نطاقات متعددة
$recentFeaturedPosts = Post::published()
->recent(30)
->featured()
->take(10)
->get();
// نطاق مع معاملات
$techPosts = Post::published()
->ofCategory('technology')
->recent(14)
->get();
// دمج النطاقات مع طرق الاستعلام العادية
$popularTechPosts = Post::published()
->ofCategory('technology')
->where('views', '>', 1000)
->orderBy('created_at', 'desc')
->paginate(15);
scopePublished إلى published() عند استدعائها.
النطاقات الديناميكية (Dynamic Scopes)
النطاقات الديناميكية تسمح لك بإنشاء منطق استعلام مرن وشرطي يتكيف بناءً على المعاملات.
// app/Models/Product.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
// نطاق نطاق السعر الديناميكي
public function scopePriceBetween($query, $min, $max = null)
{
if ($max === null) {
return $query->where('price', '>=', $min);
}
return $query->whereBetween('price', [$min, $max]);
}
// نطاق الفرز الديناميكي
public function scopeSortBy($query, $column, $direction = 'asc')
{
$allowedColumns = ['name', 'price', 'created_at', 'popularity'];
if (!in_array($column, $allowedColumns)) {
$column = 'created_at';
}
return $query->orderBy($column, $direction);
}
// نطاق التصفية الديناميكي
public function scopeFilter($query, array $filters)
{
return $query->when($filters['category'] ?? null, function ($query, $category) {
$query->where('category_id', $category);
})->when($filters['search'] ?? null, function ($query, $search) {
$query->where('name', 'like', "%{$search}%");
})->when($filters['in_stock'] ?? null, function ($query) {
$query->where('stock', '>', 0);
});
}
// نطاق شرطي
public function scopeAvailable($query, $includeOutOfStock = false)
{
if (!$includeOutOfStock) {
$query->where('stock', '>', 0);
}
return $query->where('is_active', true);
}
}
// تصفية نطاق السعر
$affordableProducts = Product::priceBetween(10, 50)->get();
$premiumProducts = Product::priceBetween(100)->get();
// الفرز الديناميكي
$sortedProducts = Product::sortBy('price', 'desc')->get();
// التصفية المعقدة
$filteredProducts = Product::filter([
'category' => 5,
'search' => 'laptop',
'in_stock' => true
])->paginate(20);
// المنطق الشرطي
$availableProducts = Product::available()->get();
$allActiveProducts = Product::available(true)->get();
النطاقات العامة (Global Scopes)
النطاقات العامة تُطبَّق تلقائياً على جميع الاستعلامات لنموذج معين. إنها مثالية لتنفيذ الحذف الناعم أو تعدد المستأجرين أو التصفية الافتراضية.
// app/Models/Scopes/ActiveScope.php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class ActiveScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('is_active', true);
}
}
// app/Models/Scopes/TenantScope.php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
if (auth()->check()) {
$builder->where('tenant_id', auth()->user()->tenant_id);
}
}
}
// app/Models/Product.php
namespace App\Models;
use App\Models\Scopes\ActiveScope;
use App\Models\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected static function booted()
{
// إضافة نطاق عام باستخدام الفئة
static::addGlobalScope(new ActiveScope);
// إضافة نطاق عام باستخدام closure
static::addGlobalScope('tenant', function (Builder $builder) {
if (auth()->check()) {
$builder->where('tenant_id', auth()->user()->tenant_id);
}
});
}
}
// الآن جميع الاستعلامات تتضمن هذه النطاقات تلقائياً
$products = Product::all(); // المنتجات النشطة فقط للمستأجر الحالي
$product = Product::find(1); // فقط إذا كانت نشطة وتنتمي للمستأجر
// إزالة نطاق عام محدد
$allProducts = Product::withoutGlobalScope(ActiveScope::class)->get();
// إزالة نطاقات عامة متعددة
$allTenantProducts = Product::withoutGlobalScopes([
ActiveScope::class,
'tenant'
])->get();
// إزالة جميع النطاقات العامة
$absolutelyAll = Product::withoutGlobalScopes()->get();
البناة المخصصة (Custom Query Builders)
البناة المخصصة تسمح لك بتوسيع باني استعلامات Laravel بطرقك الخاصة، مما يخلق واجهة سلسة مصممة خصيصاً لمجالك.
// app/Models/Builders/PostBuilder.php
namespace App\Models\Builders;
use Illuminate\Database\Eloquent\Builder;
class PostBuilder extends Builder
{
public function published()
{
return $this->where('status', 'published');
}
public function draft()
{
return $this->where('status', 'draft');
}
public function byAuthor($authorId)
{
return $this->where('author_id', $authorId);
}
public function popular($threshold = 100)
{
return $this->where('views', '>=', $threshold)
->orderBy('views', 'desc');
}
public function withCategory()
{
return $this->with(['category' => function ($query) {
$query->select('id', 'name', 'slug');
}]);
}
public function searchByTitle($term)
{
return $this->where('title', 'like', "%{$term}%");
}
public function publishedBetween($startDate, $endDate)
{
return $this->whereBetween('published_at', [$startDate, $endDate]);
}
}
// app/Models/Post.php
namespace App\Models;
use App\Models\Builders\PostBuilder;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
// إخبار النموذج باستخدام الباني المخصص
public function newEloquentBuilder($query)
{
return new PostBuilder($query);
}
// اختياري: إضافة PHPDoc للإكمال التلقائي في IDE
/**
* @return PostBuilder
*/
public static function query()
{
return parent::query();
}
}
// الآن يمكنك استخدام طرق الباني المخصص
$popularPosts = Post::published()
->popular(500)
->withCategory()
->take(10)
->get();
$authorDrafts = Post::draft()
->byAuthor(5)
->orderBy('updated_at', 'desc')
->get();
$searchResults = Post::published()
->searchByTitle('Laravel')
->publishedBetween(now()->subMonths(3), now())
->paginate(15);
ماكرو باني الاستعلام (Query Builder Macros)
الماكرو تسمح لك بإضافة طرق مخصصة إلى باني الاستعلام عالمياً، مما يجعلها متاحة على جميع النماذج.
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
// ماكرو للترتيب حسب أعمدة متعددة
Builder::macro('orderByMultiple', function (array $columns) {
foreach ($columns as $column => $direction) {
$this->orderBy($column, $direction);
}
return $this;
});
// ماكرو لإضافة WHERE IN بشكل أكثر سلاسة
Builder::macro('whereIdIn', function (array $ids) {
return $this->whereIn('id', $ids);
});
// ماكرو للحصول على سجلات عشوائية
Builder::macro('random', function ($count = 1) {
return $this->inRandomOrder()->limit($count);
});
// ماكرو لنطاقات التاريخ الشائعة
Builder::macro('today', function () {
return $this->whereDate('created_at', today());
});
Builder::macro('thisWeek', function () {
return $this->whereBetween('created_at', [
now()->startOfWeek(),
now()->endOfWeek()
]);
});
Builder::macro('thisMonth', function () {
return $this->whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year);
});
}
}
// استخدام الماكرو على أي نموذج
$products = Product::whereIdIn([1, 5, 10, 15])->get();
$randomPosts = Post::published()->random(3)->get();
$todayOrders = Order::today()->get();
$thisWeekSales = Sale::thisWeek()->sum('amount');
$sortedProducts = Product::orderByMultiple([
'category' => 'asc',
'price' => 'desc',
'name' => 'asc'
])->get();
MacroServiceProvider) للحفاظ على AppServiceProvider نظيفاً ومنظماً.
تمرين 1: إنشاء نطاقات محلية
أنشئ نموذج User مع النطاقات المحلية التالية:
active()- إرجاع المستخدمين معis_active = trueverified()- إرجاع المستخدمين معemail_verified_atغير فارغadmins()- إرجاع المستخدمين معrole = 'admin'registeredAfter($date)- إرجاع المستخدمين المسجلين بعد تاريخ معين
ثم اكتب استعلاماً للحصول على جميع المسؤولين النشطين والمتحققين المسجلين في آخر 30 يوماً.
تمرين 2: بناء باني استعلام مخصص
أنشئ باني استعلام مخصص لنموذج Order مع هذه الطرق:
pending()- الطلبات بحالة "pending"completed()- الطلبات بحالة "completed"forCustomer($customerId)- الطلبات لعميل محددtotalOver($amount)- الطلبات التي يتجاوز مجموعها مبلغاً معيناًrecent($days = 7)- الطلبات من آخر N يوم
نفذ الباني واستخدمه للعثور على جميع الطلبات المكتملة للعميل رقم 5 بمجموع يزيد عن 100 دولار في آخر 30 يوماً.
تمرين 3: إنشاء نطاق عام
أنشئ نطاقاً عاماً يُسمى PublishedScope يقوم تلقائياً بتصفية النماذج حيث published_at ليس فارغاً وفي الماضي. طبّقه على نموذج Article، ثم اكتب كوداً من أجل:
- الحصول على جميع المقالات المنشورة (النطاق مطبق)
- الحصول على جميع المقالات بما في ذلك غير المنشورة (النطاق محذوف)
- عد المقالات المنشورة مقابل إجمالي المقالات
- النطاقات المحلية مثالية لمنطق الاستعلام القابل لإعادة الاستخدام على نماذج محددة
- النطاقات العامة تنطبق تلقائياً على جميع الاستعلامات لنموذج
- البناة المخصصة توفر واجهة سلسة خاصة بالمجال
- ماكرو باني الاستعلام تمدد الوظائف عبر جميع النماذج
- كل هذه التقنيات تساعد في الحفاظ على كودك DRY وسهل الصيانة