تقسيم النتائج والتصفية والبحث Pagination وFiltering وSearching في Laravel
عندما تبدأ في بناء تطبيق باستخدام Laravel، قد تحتاج إلى عرض المقالات أو المنتجات أو المستخدمين الموجودين في قاعدة البيانات. في البداية، ربما تستخدم Article::all() لجلب جميع المقالات وعرضها داخل الصفحة، لكن ماذا سيحدث عندما يحتوي موقعك على آلاف المقالات أو مئات الآلاف من المنتجات؟
عرض جميع النتائج دفعة واحدة قد يؤدي إلى بطء الصفحة واستهلاك موارد الخادم، كما يجعل تصفح البيانات صعبًا على المستخدم. لذلك توفر Laravel أدوات تساعدك على تقسيم النتائج إلى صفحات، وتصفية البيانات حسب شروط معينة، والبحث عن سجلات محددة.
في هذا الدرس سنتعلم ثلاثة مفاهيم أساسية:
- Pagination: تقسيم النتائج إلى صفحات صغيرة.
- Filtering: تصفية النتائج وفق شروط، مثل التصنيف أو الحالة.
- Searching: البحث عن كلمة داخل عنوان المقال أو محتواه.
سنطبق هذه المفاهيم على نظام المقالات الذي استخدمناه في الدروس السابقة، بالاعتماد على Eloquent ORM وControllers وBlade Templates. ويمكنك مراجعة توثيق Laravel الرسمي الخاص بالترقيم وQuery Builder للتوسع في هذه الموضوعات.
أولًا: ما المقصود بـ Pagination؟
Pagination هي عملية تقسيم مجموعة كبيرة من النتائج إلى صفحات متعددة بدلًا من عرضها جميعًا في صفحة واحدة.
تخيل أن موقعك يحتوي على 500 مقال، وتريد عرض 10 مقالات فقط في كل صفحة. بدلًا من عرض المقالات الخمسمائة دفعة واحدة، ستعرض أول 10 مقالات، ثم تتيح للمستخدم الانتقال إلى الصفحة التالية لعرض المقالات التالية.
تساعد هذه الطريقة على:
- تحسين تجربة المستخدم.
- تقليل عدد السجلات التي تُعرض في كل صفحة.
- تنظيم النتائج وتسهيل تصفحها.
- تجنب تحميل مجموعة ضخمة من البيانات وعرضها دفعة واحدة.
توفر Laravel عدة طرق للترقيم، أهمها paginate() وsimplePaginate() وcursorPaginate()، ولكل طريقة استخدام يناسب نوع التطبيق وحجم البيانات. المصدر الرسمي
لماذا لا نستخدم all() دائمًا؟
يمكنك جلب جميع المقالات بهذه الطريقة:
$articles = Article::all();
لكن هذه الطريقة تجلب جميع السجلات المطابقة من قاعدة البيانات، ثم تحتفظ بالنتائج في الذاكرة. وقد يكون ذلك غير مناسب عندما يكبر حجم البيانات.
أما عند استخدام paginate():
$articles = Article::paginate(10);
فإن Laravel تجلب مجموعة النتائج الخاصة بالصفحة الحالية، وتوفر معلومات عن عدد الصفحات والنتائج الإجمالية، حتى تتمكن من إنشاء روابط التنقل بينها.
ثانيًا: إنشاء Pagination باستخدام paginate()
الخطوة الأولى: تجهيز Controller
افتح Controller الخاص بالمقالات، أو أنشئ واحدًا جديدًا إذا لم يكن موجودًا:
php artisan make:controller ArticleController
افتح الملف:
app/Http/Controllers/ArticleController.php
ثم أضف الكود التالي:
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\View\View;
class ArticleController extends Controller
{
public function index(): View
{
$articles = Article::latest()->paginate(10);
return view('articles.index', compact('articles'));
}
}
شرح الكود:
Article::latest()يرتب المقالات من الأحدث إلى الأقدم اعتمادًا علىcreated_atافتراضيًا.paginate(10)يقسم النتائج إلى صفحات، بحيث تعرض كل صفحة 10 مقالات كحد أقصى.compact('articles')يمرر متغير المقالات إلى صفحة Blade.view('articles.index')يعرض ملف الواجهة المطلوب.
إذا كان جدول المقالات يحتوي على 47 مقالًا، فسيكون عدد الصفحات خمس صفحات عند عرض 10 مقالات في كل صفحة.
الخطوة الثانية: إنشاء Route
افتح الملف routes/web.php وأضف:
use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;
Route::get('/articles', [ArticleController::class, 'index'])
->name('articles.index');
أصبح بإمكان المستخدم الوصول إلى قائمة المقالات عبر الرابط:
http://localhost:8000/articles
وعند الانتقال إلى الصفحة الثانية، ستضيف Laravel معامل الصفحة إلى عنوان الرابط، مثل:
http://localhost:8000/articles?page=2
تتعامل Laravel مع هذا المعامل تلقائيًا عند استخدام paginate().
الخطوة الثالثة: عرض المقالات في Blade
أنشئ الملف التالي إذا لم يكن موجودًا:
resources/views/articles/index.blade.php
ثم أضف:
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>المقالات</title>
</head>
<body>
<h1>قائمة المقالات</h1>
@forelse ($articles as $article)
<article>
<h2>{{ $article->title }}</h2>
<p>
{{ $article->content }}
</p>
<small>
{{ $article->created_at?->format('Y-m-d') }}
</small>
</article>
<hr>
@empty
<p>لا توجد مقالات لعرضها.</p>
@endforelse
{{ $articles->links() }}
</body>
</html>
السطر الأهم في هذا المثال هو:
{{ $articles->links() }}
تستخدم Laravel هذه الدالة لعرض روابط التنقل بين الصفحات. ويعتمد الشكل الافتراضي لهذه الروابط على Tailwind CSS، لذلك قد تحتاج إلى تنسيقها إذا كنت تستخدم إطار CSS آخر أو تصميمًا مخصصًا.
ثالثًا: الفرق بين paginate() وsimplePaginate() وcursorPaginate()
1. استخدام paginate()
$articles = Article::paginate(10);
تُستخدم عندما تريد عرض روابط الصفحات مع معرفة العدد الإجمالي للنتائج وعدد الصفحات.
وهي مناسبة لقوائم المقالات والمنتجات وصفحات الإدارة التي تحتاج إلى معرفة عدد الصفحات.
2. استخدام simplePaginate()
$articles = Article::simplePaginate(10);
تُستخدم عندما تحتاج إلى روابط الصفحة التالية والسابقة فقط، ولا تحتاج إلى معرفة العدد الإجمالي للنتائج.
قد تكون أكثر كفاءة في بعض الحالات لأنها لا تنفذ استعلام حساب العدد الإجمالي للنتائج بالطريقة التي تستخدمها paginate().
3. استخدام cursorPaginate()
$articles = Article::orderBy('id')->cursorPaginate(10);
تستخدم هذه الطريقة مؤشرًا للتنقل بين النتائج بدلًا من الاعتماد على أرقام الصفحات التقليدية. وقد تكون مناسبة لقوائم البيانات الكبيرة جدًا أو واجهات التحميل المستمر.
تتطلب هذه الطريقة ترتيبًا مناسبًا، ويجب أن تكون أعمدة الترتيب مؤهلة لدعم التصفح بالمؤشر، مثل استخدام عمود فريد أو مجموعة أعمدة تضمن ترتيبًا فريدًا. كما أنها لا توفر روابط لجميع أرقام الصفحات بالطريقة التقليدية.
لذلك، ابدأ باستخدام paginate() في تطبيقك، وانتقل إلى الطرق الأخرى عندما تكون هناك حاجة فعلية إليها.
رابعًا: ما المقصود بـ Filtering؟
Filtering يعني تصفية النتائج وفق شروط محددة يختارها المستخدم أو يفرضها التطبيق.
على سبيل المثال، في موقع المقالات قد تريد عرض:
- المقالات المنشورة فقط.
- المقالات التابعة إلى تصنيف محدد.
- المقالات التي أُنشئت خلال فترة زمنية معينة.
- المقالات التي كتبها مستخدم محدد.
يمكنك تنفيذ هذه الشروط باستخدام Eloquent وطرق مثل where() وwhen().
مثال: تصفية المقالات حسب الحالة
لنفترض أن جدول المقالات يحتوي على عمود اسمه status، وقيمته إما published أو draft.
يمكنك جلب المقالات المنشورة فقط باستخدام:
$articles = Article::where('status', 'published')
->latest()
->paginate(10);
هنا لن تعرض Laravel إلا المقالات التي تحقق الشرط، ثم تقسم النتائج المطابقة إلى صفحات.
ملاحظة: يجب أن يكون عمود status موجودًا فعلًا في جدول المقالات. إذا لم يكن موجودًا، فيجب إضافته من خلال Migration وتحديث بنية قاعدة البيانات.
مثال: التصفية حسب اختيار المستخدم
لنفترض أنك تريد السماح للمستخدم باختيار حالة المقال من خلال الرابط:
/articles?status=published
يمكنك قراءة قيمة الحالة من الطلب، ثم استخدامها في الاستعلام.
public function index()
{
$status = request('status');
$articles = Article::query()
->when($status, function ($query, $status) {
$query->where('status', $status);
})
->latest()
->paginate(10);
return view('articles.index', compact('articles', 'status'));
}
تعمل when() على تطبيق الشرط فقط عندما تكون قيمة $status موجودة وصادقة وفق تقييم PHP.
لكن في التطبيقات الحقيقية، يجب ألا نسمح للمستخدم بإرسال أي قيمة عشوائية إذا كانت الحالات محددة مسبقًا. لذلك يمكن التحقق من القيمة أولًا:
$status = request('status');
if (! in_array($status, ['published', 'draft'], true)) {
$status = null;
}
بعد ذلك يمكنك استخدام $status في الاستعلام السابق.
هذا المثال بسيط للتعلم، أما عند بناء تطبيق أكبر، فيمكن تنظيم التحقق من مدخلات التصفية باستخدام Validation أو Form Request.
إنشاء نموذج تصفية في Blade
أضف النموذج التالي قبل قائمة المقالات في ملف articles/index.blade.php:
<form action="{{ route('articles.index') }}" method="GET">
<label for="status">حالة المقال:</label>
<select name="status" id="status">
<option value="">جميع المقالات</option>
<option value="published"
@selected(($status ?? '') === 'published')>
منشور
</option>
<option value="draft"
@selected(($status ?? '') === 'draft')>
مسودة
</option>
</select>
<button type="submit">تصفية</button>
</form>
استخدمنا GET لأننا نريد تمرير خيارات البحث والتصفية ضمن عنوان الصفحة. وهذا يسمح بمشاركة رابط النتائج مع شخص آخر أو إعادة فتحه لاحقًا.
خامسًا: ما المقصود بـ Searching؟
Searching هو البحث عن كلمة أو عبارة داخل البيانات.
في موقع المقالات، قد يكتب المستخدم كلمة مثل Laravel، ويريد رؤية المقالات التي تحتوي عناوينها أو محتوياتها على هذه الكلمة.
يمكننا استخدام where() مع شرط LIKE، أو استخدام whereLike() في إصدارات Laravel التي تدعم هذه الطريقة.
مثال: البحث داخل عنوان المقال
$search = request('search');
$articles = Article::query()
->when($search, function ($query, $search) {
$query->whereLike('title', "%{$search}%");
})
->latest()
->paginate(10);
في هذا المثال:
request('search')تقرأ عبارة البحث من الطلب.whereLike()تبحث عن العبارة داخل عمود العنوان.- علامتا
%تسمحان بوجود نص قبل الكلمة أو بعدها. paginate(10)يقسم نتائج البحث إلى صفحات.
على سبيل المثال، إذا كتب المستخدم Laravel، فسيظهر أي مقال يحتوي عنوانه على هذه الكلمة.
وتوفر Laravel طرقًا أخرى مثل orWhereLike() للبحث في أعمدة متعددة، مع إمكانية التحكم في حساسية حالة الأحرف بحسب الحاجة. راجع توثيق Query Builder للتفاصيل.
البحث في العنوان والمحتوى معًا
قد ترغب في البحث عن الكلمة داخل عنوان المقال أو محتواه. يمكننا تجميع شروط البحث داخل دالة واحدة:
$search = request('search');
$articles = Article::query()
->when($search, function ($query, $search) {
$query->where(function ($query) use ($search) {
$query->whereLike('title', "%{$search}%")
->orWhereLike('content', "%{$search}%");
});
})
->latest()
->paginate(10);
استخدمنا where() مع دالة داخلية لتجميع الشرطين منطقيًا. وهذا مهم حتى لا يؤثر شرط OR في شروط أخرى قد نضيفها للاستعلام، مثل حالة النشر أو ملكية المقال.
إذا كنت تريد عرض المقالات المنشورة فقط والبحث داخلها، فستكون الصيغة:
$articles = Article::query()
->where('status', 'published')
->when($search, function ($query, $search) {
$query->where(function ($query) use ($search) {
$query->whereLike('title', "%{$search}%")
->orWhereLike('content', "%{$search}%");
});
})
->latest()
->paginate(10);
بهذه الطريقة، لا تظهر المسودات حتى لو كانت تحتوي على كلمة البحث.
سادسًا: إنشاء نموذج البحث في Blade
افتح الملف resources/views/articles/index.blade.php وأضف نموذج البحث:
<form action="{{ route('articles.index') }}" method="GET">
<label for="search">ابحث عن مقال:</label>
<input
type="search"
name="search"
id="search"
value="{{ request('search') }}"
placeholder="اكتب عنوان المقال..."
>
<button type="submit">بحث</button>
</form>
لاحظ استخدام:
value="{{ request('search') }}"
هذا يجعل عبارة البحث السابقة تظهر داخل الحقل بعد إرسال النموذج، بدلًا من مطالبة المستخدم بكتابتها مرة أخرى.
ويمكنك الجمع بين البحث والتصفية داخل نموذج واحد، بحيث يختار المستخدم حالة المقال ويكتب كلمة البحث ثم يضغط على زر البحث.
سابعًا: الحفاظ على خيارات البحث والتصفية عند الانتقال بين الصفحات
هذه من أكثر النقاط التي يواجهها المبتدئون.
لنفترض أن المستخدم بحث عن كلمة Laravel، وظهرت له عدة صفحات من النتائج. عندما ينتقل إلى الصفحة الثانية، يجب أن يبقى شرط البحث موجودًا حتى لا تظهر جميع المقالات بدلًا من النتائج المطابقة.
تتيح Laravel استخدام withQueryString() للاحتفاظ بمعاملات الاستعلام الحالية عند إنشاء روابط الصفحات:
$articles = Article::query()
->when($search, function ($query, $search) {
$query->whereLike('title', "%{$search}%");
})
->latest()
->paginate(10)
->withQueryString();
إذا كان الرابط يحتوي على:
/articles?search=Laravel&status=published
فستحتفظ روابط الصفحات بمعاملات الاستعلام الحالية.
يمكنك أيضًا استخدام appends() لإضافة معاملات محددة يدويًا:
$articles->appends([
'search' => $search,
'status' => $status,
]);
استخدم طريقة واحدة مناسبة لحالتك؛ غالبًا ما تكون withQueryString() أسهل عندما تريد الاحتفاظ بجميع خيارات البحث والتصفية الموجودة في الرابط.
ثامنًا: بناء صفحة مقالات تجمع Pagination و Filtering و Searching
سننشئ الآن مثالًا متكاملًا يجمع المفاهيم الثلاثة في Controller واحد.
يفترض المثال وجود Model باسم Article وجدول يحتوي على الأعمدة title وcontent وstatus وcreated_at.
كود Controller
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ArticleController extends Controller
{
public function index(Request $request): View
{
$validated = $request->validate([
'search' => ['nullable', 'string', 'max:100'],
'status' => ['nullable', 'in:published,draft'],
]);
$search = $validated['search'] ?? null;
$status = $validated['status'] ?? null;
$articles = Article::query()
->when($search, function ($query, $search) {
$query->where(function ($query) use ($search) {
$query->whereLike(
'title',
"%{$search}%"
)->orWhereLike(
'content',
"%{$search}%"
);
});
})
->when($status, function ($query, $status) {
$query->where('status', $status);
})
->latest()
->paginate(10)
->withQueryString();
return view('articles.index', compact(
'articles',
'search',
'status'
));
}
}
شرح ما يحدث:
- نتحقق من قيمة البحث حتى لا تتجاوز 100 حرف، ونسمح بغيابها.
- نتحقق من أن الحالة إما
publishedأوdraftإذا أُرسلت. - نبني الاستعلام باستخدام
Article::query(). - نضيف شروط البحث عند وجود كلمة بحث.
- نضيف شرط الحالة عند اختيار حالة معينة.
- نرتب المقالات من الأحدث إلى الأقدم.
- نقسم النتائج إلى صفحات، مع الاحتفاظ بخيارات البحث والتصفية.
كود Blade
يمكنك استخدام النموذج التالي في resources/views/articles/index.blade.php:
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>المقالات</title>
</head>
<body>
<h1>المقالات</h1>
<form action="{{ route('articles.index') }}" method="GET">
<input
type="search"
name="search"
value="{{ $search ?? '' }}"
placeholder="ابحث في المقالات..."
>
<select name="status">
<option value="">جميع الحالات</option>
<option value="published"
@selected(($status ?? '') === 'published')>
منشور
</option>
<option value="draft"
@selected(($status ?? '') === 'draft')>
مسودة
</option>
</select>
<button type="submit">تطبيق</button>
<a href="{{ route('articles.index') }}">مسح الفلاتر</a>
</form>
<hr>
@forelse ($articles as $article)
<article>
<h2>{{ $article->title }}</h2>
<p>{{ $article->content }}</p>
<small>الحالة: {{ $article->status }}</small>
</article>
<hr>
@empty
<p>لا توجد مقالات تطابق معايير البحث.</p>
@endforelse
{{ $articles->links() }}
</body>
</html>
أصبح بإمكان المستخدم البحث عن كلمة، وتحديد حالة المقال، والتنقل بين صفحات النتائج مع الاحتفاظ بخياراته.
تاسعًا: تحسين الأداء عند التعامل مع بيانات كثيرة
عندما يكبر التطبيق، من المهم التفكير في كفاءة الاستعلامات، وليس فقط في صحة النتائج.
1. لا تجلب جميع السجلات دون حاجة
استخدم paginate() بدلًا من all() عندما تعرض قائمة كبيرة من السجلات.
2. اختر الأعمدة التي تحتاجها
إذا لم تكن بحاجة إلى جميع أعمدة المقال، يمكنك تحديد المطلوب:
$articles = Article::query()
->select(['id', 'title', 'created_at'])
->latest()
->paginate(10);
تأكد من تضمين الأعمدة اللازمة للعلاقات أو وظائف الصفحة.
3. فكر في الفهارس Indexes
إذا كان البحث أو التصفية يعتمد على أعمدة معينة بصورة متكررة، فقد تساعد الفهارس في تحسين بعض الاستعلامات. لكن فائدتها تعتمد على نوع قاعدة البيانات وحجم البيانات وطبيعة الاستعلام.
كذلك، فإن استخدام LIKE مع % في بداية عبارة البحث، مثل %Laravel%، قد يمنع الاستفادة من فهرس تقليدي في بعض قواعد البيانات. إذا كنت تحتاج إلى بحث متقدم على نطاق كبير، ففكر في حلول Full-Text Search المناسبة لقاعدة البيانات أو أدوات البحث المتخصصة.
4. لا تسمح للمستخدم بتحديد أعمدة الترتيب دون تحقق
إذا أردت إضافة قائمة لترتيب المقالات حسب العنوان أو التاريخ، فلا تمرر اسم العمود القادم من المستخدم مباشرة إلى orderBy().
بدلًا من ذلك، استخدم قائمة مسموحًا بها:
$allowedSorts = ['title', 'created_at'];
$sort = request('sort', 'created_at');
if (! in_array($sort, $allowedSorts, true)) {
$sort = 'created_at';
}
$articles = Article::orderBy($sort, 'desc')
->paginate(10)
->withQueryString();
تذكر أن أسماء الأعمدة لا تُحمى بالطريقة نفسها التي تُحمى بها قيم المقارنة عبر parameter binding؛ لذلك يجب تقييدها إلى خيارات موثوقة. توثيق Query Builder
عاشرًا: أخطاء شائعة يجب تجنبها
الخطأ الأول: استخدام links() مع مجموعة عادية
إذا جلبت النتائج باستخدام all()، فلن تحصل على كائن Pagination الذي توفره paginate().
الحل:
$articles = Article::paginate(10);
ثم في Blade:
{{ $articles->links() }}
الخطأ الثاني: فقدان البحث بعد تغيير الصفحة
السبب هو عدم الاحتفاظ بمعاملات الاستعلام.
الحل:
$articles = Article::paginate(10)->withQueryString();
الخطأ الثالث: عدم تجميع شروط OR
قد يؤدي استخدام orWhereLike() دون تجميع الشروط إلى ظهور نتائج لا تحقق بقية شروط الاستعلام.
الحل هو استخدام دالة داخلية مع where() لتجميع شروط البحث.
الخطأ الرابع: نسيان التحقق من المدخلات
لا تفترض أن المستخدم سيرسل قيمًا صحيحة دائمًا. استخدم Validation لتحديد نوع البيانات وطول عبارة البحث والقيم المسموح بها للتصفية.
الخطأ الخامس: الاعتماد على LIKE لكل أنواع البحث
البحث البسيط مناسب لكثير من التطبيقات الصغيرة والمتوسطة، لكنه ليس دائمًا الخيار الأفضل للبحث الكامل داخل نصوص كبيرة أو لآلاف السجلات ذات متطلبات بحث متقدمة.
الحادي عشر: تمرين عملي
حان وقت التطبيق!
أنشئ صفحة لإدارة المقالات في مشروع Laravel الخاص بك، وطبّق المتطلبات التالية:
- اعرض 10 مقالات في كل صفحة.
- أضف حقلًا للبحث داخل عنوان المقال.
- أضف قائمة لتصفية المقالات حسب الحالة.
- اجعل البحث والتصفية يعملان معًا.
- احتفظ بخيارات المستخدم عند الانتقال إلى الصفحة التالية.
- أضف رابطًا لمسح جميع الفلاتر.
- اعرض رسالة مناسبة إذا لم توجد نتائج.
- تحقق من مدخلات المستخدم باستخدام Validation.
بعد الانتهاء، جرّب البحث عن كلمة موجودة في عدة مقالات، ثم اختر حالة معينة، وانتقل إلى الصفحة التالية. تأكد من استمرار تطبيق الشروط نفسها.
تعلمنا في هذا الدرس كيفية تنظيم نتائج قاعدة البيانات باستخدام Pagination، وإنشاء شروط Filtering باستخدام where() وwhen()، وإضافة Searching باستخدام whereLike() وorWhereLike().
كما أنشأنا صفحة متكاملة للمقالات تجمع البحث والتصفية والترقيم، وتعلمنا أهمية الاحتفاظ بمعاملات الاستعلام، والتحقق من المدخلات، وتحسين أداء الاستعلامات عند نمو التطبيق.
أصبحت لديك الآن الأدوات الأساسية لبناء صفحات القوائم في مواقع المقالات والمتاجر ولوحات التحكم.
في الدرس السادس عشر، سننتقل إلى موضوع مهم عند بناء تطبيقات Laravel، وهو إرسال البريد الإلكتروني (Mail) والإشعارات (Notifications)، وكيفية إرسال رسائل للمستخدمين عند التسجيل أو نشر مقال أو حدوث حدث مهم داخل التطبيق.
