عند بناء REST API، من الأخطاء الشائعة إرجاع جميع البيانات الموجودة في قاعدة البيانات داخل Request واحد.
إذا كان لدينا جدول Products يحتوي على 20 Record فقط، قد لا تظهر مشكلة واضحة عند استخدام:
Product::all();لكن ماذا لو أصبح لدينا:
10,000 Products
100,000 Orders
1,000,000 Transactionsإرجاع جميع البيانات في Request واحد يؤدي إلى استهلاك أكبر للذاكرة وقاعدة البيانات والشبكة، كما يجعل Response أكبر وأبطأ.
الحل هو:
Paginationأي تقسيم النتائج إلى Pages وإرجاع عدد محدود من Records في كل Request.
Laravel يوفر Pagination متكاملة مع Eloquent وQuery Builder وAPI Resources، مما يجعل بناء Paginated REST API بسيطًا ومنظمًا.
جدول المحتويات
- لماذا نحتاج Pagination؟
- استخدام paginate()
- التنقل بين الصفحات
- تحديد عدد العناصر
- Pagination مع API Resource
- شكل JSON Response
- ما هي links؟
- ما هي meta؟
- simplePaginate()
- cursorPaginate()
- الفرق بين أنواع Pagination
- السماح للـClient بتحديد per_page
- تحديد Maximum per_page
- Validation للـPagination
- Pagination مع Filtering
- Pagination مع Search
- Pagination مع Sorting
- الحفاظ على Query String
- تخصيص Pagination Response
- Pagination والعلاقات
- الأداء
- مثال متكامل
- أفضل الممارسات
- ملخص الدرس
لماذا نحتاج Pagination؟
لنفترض أن لدينا:
Product::all();إذا كان الجدول يحتوي على 50,000 Product، سيحاول Laravel جلب جميع النتائج.
ثم سيتم تحويلها إلى Models، وبعدها إلى API Resources وJSON وإرسالها عبر الشبكة.
بدل ذلك يمكن إرجاع 20 عنصرًا فقط:
Product::paginate(20);ثم يطلب Client الصفحة التالية عندما يحتاج إليها.
استخدام paginate()
أبسط مثال:
public function index()
{
$products = Product::query()
->paginate(20);
return ProductResource::collection(
$products
);
}هذا يعني:
20 Products Per Pageالتنقل بين الصفحات
Laravel يستخدم افتراضيًا Query Parameter باسم:
pageالصفحة الأولى:
GET /api/products?page=1الثانية:
GET /api/products?page=2الثالثة:
GET /api/products?page=3Laravel يقرأ قيمة page تلقائيًا.
تحديد عدد العناصر في الصفحة
يمكن تحديده مباشرة:
Product::paginate(15);أو:
Product::paginate(50);لكن في API حقيقي قد نريد السماح لـClient باختيار العدد ضمن حدود معينة.
Pagination مع API Resource
لا نحتاج إلى التخلي عن API Resources عند استخدام Pagination.
يمكن تمرير Paginator مباشرة إلى:
ProductResource::collection()مثل:
return ProductResource::collection(
Product::query()
->latest()
->paginate(20)
);Laravel سيقوم بتحويل Products باستخدام ProductResource مع الاحتفاظ ببيانات Pagination.
شكل JSON Response
عند استخدام Paginated Resource Collection، تكون الاستجابة قريبة من:
{
"data": [
{
"id": 1,
"name": "Product A"
},
{
"id": 2,
"name": "Product B"
}
],
"links": {
"first": "...?page=1",
"last": "...?page=10",
"prev": null,
"next": "...?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 10,
"per_page": 20,
"to": 20,
"total": 200
}
}وهذا أحد الأسباب المهمة لاستخدام Laravel Pagination مع API Resources؛ فالـClient يحصل على البيانات ومعلومات التنقل في Response منظمة.
ما هي links؟
تحتوي:
linksعلى URLs تساعد Client على الانتقال بين الصفحات.
مثل:
"first"
"last"
"prev"
"next"يمكن لتطبيق الهاتف مثلًا استخدام:
nextلمعرفة إن كان هناك Page إضافية.
ما هي meta؟
تحتوي:
metaعلى معلومات عن Pagination.
مثل:
current_page
last_page
per_page
totalيمكن للواجهة استخدام هذه المعلومات لإنشاء Pagination Controls مثل:
Previous
1
2
3
4
5
Nextاستخدام simplePaginate()
في بعض الحالات لا نحتاج إلى معرفة العدد الإجمالي للنتائج.
يمكن استخدام:
Product::simplePaginate(20);الفرق المهم أن:
paginate()يحتاج إلى معرفة العدد الإجمالي للنتائج لإنشاء معلومات مثل Total Pages.
أما:
simplePaginate()فيركز على معرفة وجود الصفحة السابقة أو التالية، ويتجنب Query الخاص بحساب إجمالي عدد النتائج.
قد يكون هذا مناسبًا عندما لا تحتاج الواجهة إلى:
Page 1 of 5000وإنما تحتاج فقط:
Next
Previousاستخدام cursorPaginate()
Laravel يوفر أيضًا:
cursorPaginate()مثل:
$products = Product::query()
->orderBy('id')
->cursorPaginate(20);في هذه الطريقة لا يعتمد التنقل على Offset بالشكل التقليدي.
بدل:
?page=10يحصل Client على Cursor:
?cursor=...يمثل موضعه الحالي في مجموعة البيانات.
هذا النوع مفيد خصوصًا في:
- Large Datasets.
- Infinite Scrolling.
- Feeds.
- Activity Logs.
- Transactions.
- بيانات تتغير باستمرار.
ويجب أن يحتوي Query على:
orderBy()مناسب لاستخدام Cursor Pagination.
الفرق بين أنواع Pagination
| الطريقة | الاستخدام المناسب |
|---|---|
| paginate() | عندما نحتاج إلى Total وعدد الصفحات. |
| simplePaginate() | عندما نحتاج Previous وNext بدون Total كامل. |
| cursorPaginate() | للبيانات الكبيرة وInfinite Scroll والبيانات كثيرة التغير. |
السماح للـClient بتحديد per_page
يمكن السماح للـFrontend بإرسال:
GET /api/products?per_page=50ثم:
$perPage = $request->integer(
'per_page',
20
);
$products = Product::query()
->paginate($perPage);لكن هناك مشكلة أمنية وأدائية هنا.
ماذا لو أرسل المستخدم:
?per_page=1000000سيصبح الهدف من Pagination بلا فائدة تقريبًا.
تحديد Maximum per_page
يجب وضع حد أعلى.
مثل:
$perPage = min(
$request->integer(
'per_page',
20
),
100
);الآن القيمة الافتراضية:
20والحد الأعلى:
100حتى لو أرسل Client:
?per_page=100000سيتم إرجاع 100 فقط.
Validation للـPagination Parameters
يمكن جعل الأمر أوضح باستخدام Validation:
$validated = $request->validate([
'page' => [
'sometimes',
'integer',
'min:1',
],
'per_page' => [
'sometimes',
'integer',
'min:1',
'max:100',
],
]);ثم:
$perPage =
$validated['per_page'] ?? 20;بهذا إذا أرسل Client:
?per_page=50000يحصل على Validation Error بدل السماح بالقيمة.
Pagination مع Filtering
يمكن استخدام Pagination بعد تطبيق Filters.
مثل:
GET /api/products?status=active&page=2وفي Query:
$products = Product::query()
->when(
$request->status,
function ($query, $status) {
$query->where(
'status',
$status
);
}
)
->paginate(20);يتم تطبيق Filter أولًا، ثم Pagination على النتائج.
Pagination مع Search
يمكن دعم:
GET /api/products?search=iphone&page=1مثلًا:
$products = Product::query()
->when(
$request->filled('search'),
function ($query) use ($request) {
$query->where(
'name',
'like',
'%' . $request->search . '%'
);
}
)
->paginate(20);Pagination مع Sorting
يجب أن يكون ترتيب النتائج ثابتًا وواضحًا.
مثل:
$products = Product::query()
->latest()
->paginate(20);أو:
$products = Product::query()
->orderBy('name')
->paginate(20);خصوصًا مع Cursor Pagination، يعتبر ترتيب النتائج جزءًا أساسيًا من آلية Pagination نفسها.
الحفاظ على Query Parameters داخل روابط Pagination
لنفترض أن Request هو:
/api/products?status=active&page=1قد نريد الاحتفاظ بـ:
status=activeداخل Pagination URLs.
يمكن استخدام:
->withQueryString()مثل:
$products = Product::query()
->where(
'status',
'active'
)
->paginate(20)
->withQueryString();أو إضافة Parameters محددة باستخدام:
->appends([
'status' => 'active',
]);تخصيص Pagination Response
إذا كان الشكل الافتراضي مناسبًا، فلا يوجد سبب لإعادة بناء Pagination يدويًا.
لكن في بعض المشاريع نحتاج إلى API Contract خاص.
يمكن إنشاء Resource Collection:
php artisan make:resource ProductCollectionثم تخصيص المعلومات الإضافية حسب احتياجات المشروع.
مثلًا يمكن إضافة:
'additional_meta' => [
'api_version' => 'v1',
]لكن يفضل عدم حذف معلومات Pagination المهمة إلا إذا كان لديك Contract واضح مع Frontend.
Pagination والعلاقات
يمكن دمج Pagination مع Eager Loading:
$products = Product::query()
->with([
'category',
'brand',
])
->latest()
->paginate(20);ثم:
return ProductResource::collection(
$products
);داخل ProductResource يمكن استخدام:
'category' =>
new CategoryResource(
$this->whenLoaded('category')
),وهكذا نجمع بين:
Pagination
+
Eager Loading
+
API ResourcesPagination وتحسين أداء API
Pagination خطوة مهمة، لكنها ليست الحل الوحيد لتحسين الأداء.
يجب أيضًا الانتباه إلى:
- Database Indexes.
- N+1 Queries.
- Eager Loading.
- اختيار Columns المطلوبة فقط عند الحاجة.
- عدم السماح بقيمة per_page غير محدودة.
- اختيار Pagination Type المناسب.
- Database Query Performance.
مثلًا:
Product::query()
->select([
'id',
'name',
'price',
'created_at',
])
->latest()
->paginate(20);قد يكون أفضل من تحميل عشرات Columns لا يحتاجها Endpoint.
مثال متكامل
سننشئ Endpoint يدعم:
Pagination
Search
Status Filter
per_pageمثل:
GET /api/v1/products
?search=iphone
&status=active
&per_page=20
&page=1Controller
public function index(Request $request)
{
$validated = $request->validate([
'search' => [
'sometimes',
'string',
'max:100',
],
'status' => [
'sometimes',
'string',
],
'page' => [
'sometimes',
'integer',
'min:1',
],
'per_page' => [
'sometimes',
'integer',
'min:1',
'max:100',
],
]);
$perPage =
$validated['per_page'] ?? 20;
$products = Product::query()
->with([
'category',
'brand',
])
->when(
$validated['search'] ?? null,
function ($query, $search) {
$query->where(
'name',
'like',
'%' . $search . '%'
);
}
)
->when(
$validated['status'] ?? null,
function ($query, $status) {
$query->where(
'status',
$status
);
}
)
->latest()
->paginate($perPage)
->withQueryString();
return ProductResource::collection(
$products
);
}ProductResource
public function toArray(
Request $request
): array {
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'category' =>
new CategoryResource(
$this->whenLoaded(
'category'
)
),
'brand' =>
new BrandResource(
$this->whenLoaded(
'brand'
)
),
];
}Response
{
"data": [
{
"id": 50,
"name": "iPhone",
"price": 999,
"category": {
"id": 2,
"name": "Phones"
},
"brand": {
"id": 1,
"name": "Apple"
}
}
],
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": "..."
},
"meta": {
"current_page": 1,
"last_page": 4,
"per_page": 20,
"total": 65
}
}أفضل الممارسات
- لا تستخدم all() للـEndpoints التي يمكن أن تحتوي على عدد كبير من Records.
- ضع Default مناسبًا لـper_page.
- ضع Maximum Limit لـper_page.
- استخدم Validation للـPagination Parameters.
- استخدم API Resources مع Paginator بدل بناء JSON يدويًا.
- استخدم simplePaginate عندما لا تحتاج Total.
- فكر في cursorPaginate للـInfinite Scroll والبيانات الكبيرة.
- حدد ترتيبًا ثابتًا للبيانات.
- استخدم Eager Loading لتجنب N+1.
- أضف Database Indexes للأعمدة المستخدمة بكثرة في Filtering وSorting.
ملخص الدرس
| الأداة | الاستخدام |
|---|---|
| paginate() | Pagination كاملة تحتوي على Total ومعلومات الصفحات. |
| simplePaginate() | Pagination أخف عندما نحتاج Previous وNext فقط. |
| cursorPaginate() | مناسبة للبيانات الكبيرة وInfinite Scrolling. |
| page | رقم الصفحة في Offset Pagination. |
| per_page | عدد العناصر المطلوب إرجاعها في الصفحة. |
| links | روابط التنقل بين الصفحات. |
| meta | معلومات حالة Pagination. |
| withQueryString() | الحفاظ على Query Parameters في روابط Pagination. |
| appends() | إضافة Query Parameters محددة إلى روابط Pagination. |
الخلاصة
Pagination جزء أساسي من تصميم REST API عندما نتعامل مع Collections يمكن أن يزداد حجمها مع الوقت.
بدل إرجاع جميع Records باستخدام:
Model::all()يمكن استخدام:
Model::paginate(20)ثم تمرير Paginator مباشرة إلى:
ProductResource::collection()ليحصل Client على البيانات بالإضافة إلى معلومات Pagination مثل links وmeta.
كما يوفر Laravel:
simplePaginate()
cursorPaginate()لاستخدامها حسب طبيعة البيانات وطريقة عرضها.
ومع API حقيقي يجب أيضًا وضع حد أعلى لـper_page، والتحقق من Parameters، واستخدام Eager Loading وDatabase Indexes وSorting مناسب للحصول على API سريع ومستقر حتى مع زيادة حجم البيانات.
التعليقات (0)
لا توجد تعليقات بعد — كن أول من يشارك رأيه.
أضف تعليقك