Laravel المتقدم

Eloquent المتقدم: نطاقات الاستعلام والبناة

18 دقيقة الدرس 1 من 40

مقدمة إلى تقنيات الاستعلام المتقدمة

مع نمو تطبيقات 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);
قاعدة التسمية: عند استدعاء النطاقات، تستخدم اسم الطريقة بدون بادئة "scope". يقوم Laravel تلقائياً بتحويل 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 = true
  • verified() - إرجاع المستخدمين مع 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 وسهل الصيانة