مقدمة إلى ترقيم صفحات الـ API
يعد ترقيم الصفحات ضروريًا لأي واجهة برمجة تطبيقات ترجع مجموعات من البيانات. بدون ترقيم الصفحات، يمكن أن يؤدي إرجاع مجموعات بيانات كبيرة إلى إرباك الخادم الخاص بك، واستهلاك عرض نطاق ترددي مفرط، وخلق تجارب مستخدم سيئة. يوفر Laravel ثلاث طرق قوية لترقيم الصفحات، كل منها مناسب لحالات استخدام مختلفة. في هذا الدرس الشامل، سنستكشف ترقيم الإزاحة، وترقيم الصفحات القائم على المؤشر، وترقيم الصفحات البسيط، ونتعلم متى وكيف نستخدم كل منها بفعالية.
لماذا يهم ترقيم الصفحات
قبل تنفيذ ترقيم الصفحات، دعونا نفهم لماذا هو حاسم لواجهات برمجة التطبيقات الإنتاجية:
فوائد ترقيم صفحات الـ API:
- الأداء: يقلل من وقت تنفيذ استعلام قاعدة البيانات واستخدام الذاكرة
- عرض النطاق الترددي: يقلل من نقل البيانات وحجم استجابة API
- تجربة المستخدم: يمكّن من تحميل الصفحة الأولية بشكل أسرع وتحميل البيانات التدريجي
- قابلية التوسع: يسمح لواجهات برمجة التطبيقات بالتعامل مع مجموعات بيانات تحتوي على ملايين السجلات
- إدارة الموارد: يمنع استنفاد موارد الخادم
- تحسين الجوّال: يقلل من استهلاك البيانات لمستخدمي الأجهزة المحمولة
ترقيم الصفحات القائم على الإزاحة (التقليدي)
ترقيم الإزاحة هو طريقة ترقيم الصفحات الأكثر شيوعًا. توفر طريقة paginate() في Laravel هذا مباشرة:
<?php
// app/Http/Controllers/Api/PostController.php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request): JsonResponse
{
// ترقيم أساسي: 15 عنصر لكل صفحة
$posts = Post::with('author:id,name')
->latest()
->paginate(15);
return response()->json($posts);
}
}
</pre>
يتضمن هيكل استجابة ترقيم الصفحات في Laravel بيانات وصفية للتنقل السهل:
{
"current_page": 1,
"data": [
{"id": 1, "title": "المنشور الأول", "author": {"id": 1, "name": "أحمد"}},
{"id": 2, "title": "المنشور الثاني", "author": {"id": 2, "name": "فاطمة"}},
// ... 13 عنصر آخر
],
"first_page_url": "http://api.example.com/posts?page=1",
"from": 1,
"last_page": 10,
"last_page_url": "http://api.example.com/posts?page=10",
"links": [
{"url": null, "label": "« السابق", "active": false},
{"url": "http://api.example.com/posts?page=1", "label": "1", "active": true},
{"url": "http://api.example.com/posts?page=2", "label": "2", "active": false},
{"url": "http://api.example.com/posts?page=2", "label": "التالي »", "active": false}
],
"next_page_url": "http://api.example.com/posts?page=2",
"path": "http://api.example.com/posts",
"per_page": 15,
"prev_page_url": null,
"to": 15,
"total": 150
}
قيم مخصصة لكل صفحة
السماح للعملاء بتحديد عدد العناصر التي يريدونها لكل صفحة:
<?php
public function index(Request $request): JsonResponse
{
$request->validate([
'per_page' => 'integer|min:1|max:100',
]);
$perPage = $request->input('per_page', 15); // افتراضي 15
$maxPerPage = 100; // منع الإساءة
$posts = Post::with('author:id,name')
->latest()
->paginate(min($perPage, $maxPerPage));
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?per_page=25
// GET /api/posts?page=2&per_page=50
</pre>
نصيحة احترافية: قم دائمًا بفرض قيمة per_page قصوى (عادةً 100) لمنع العملاء من طلب آلاف السجلات في طلب واحد، والذي يمكن أن يثقل كاهل الخادم الخاص بك.
ترقيم الصفحات البسيط
عندما لا تحتاج إلى عرض إجمالي عدد الصفحات أو السماح بالقفز المباشر للصفحة، استخدم simplePaginate(). إنه أكثر كفاءة لأنه لا ينفذ استعلام COUNT:
<?php
public function index(): JsonResponse
{
// أكثر كفاءة - لا يوجد استعلام COUNT
$posts = Post::with('author:id,name')
->latest()
->simplePaginate(15);
return response()->json($posts);
}
</pre>
هيكل استجابة ترقيم الصفحات البسيط (لاحظ غياب total و last_page):
{
"current_page": 1,
"data": [/* ... */],
"first_page_url": "http://api.example.com/posts?page=1",
"from": 1,
"next_page_url": "http://api.example.com/posts?page=2",
"path": "http://api.example.com/posts",
"per_page": 15,
"prev_page_url": null,
"to": 15
}
ترقيم الصفحات القائم على المؤشر (متقدم)
ترقيم المؤشر هو الطريقة الأكثر كفاءة لمجموعات البيانات الكبيرة والتغذيات في الوقت الفعلي. يستخدم مؤشرات مشفرة بدلاً من أرقام الصفحات، مما يجعله مثاليًا لواجهات التمرير اللانهائي:
<?php
public function index(): JsonResponse
{
// ترقيم المؤشر - الأكثر كفاءة لمجموعات البيانات الكبيرة
$posts = Post::with('author:id,name')
->latest()
->cursorPaginate(15);
return response()->json($posts);
}
</pre>
هيكل استجابة ترقيم المؤشر:
{
"data": [/* ... */],
"path": "http://api.example.com/posts",
"per_page": 15,
"next_cursor": "eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"next_page_url": "http://api.example.com/posts?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"prev_cursor": null,
"prev_page_url": null
}
الاختلافات الرئيسية:
- ترقيم الإزاحة: يستخدم أرقام الصفحات (page=2). يمكن القفز إلى أي صفحة، ولكنه غير فعال لمجموعات البيانات الكبيرة.
- ترقيم المؤشر: يستخدم مؤشرات مشفرة. لا يمكن القفز إلى صفحات عشوائية، ولكنه فعال للغاية ومتسق حتى عندما تتغير البيانات.
- ترقيم الصفحات البسيط: مثل الإزاحة ولكن بدون إجمالي العدد. جيد لواجهات "التالي/السابق" فقط.
المقارنة: متى تستخدم كل طريقة
// ترقيم الإزاحة - استخدم عندما:
// ✓ يحتاج المستخدمون إلى القفز إلى صفحات محددة
// ✓ عرض أرقام الصفحات (1، 2، 3...)
// ✓ مجموعة البيانات صغيرة نسبيًا (< 100 ألف سجل)
// ✗ تجنب للتغذيات في الوقت الفعلي (غير متسق مع البيانات الجديدة)
$users = User::paginate(20);
// ترقيم المؤشر - استخدم عندما:
// ✓ واجهات التمرير اللانهائي / "تحميل المزيد"
// ✓ مجموعات البيانات الكبيرة (ملايين السجلات)
// ✓ التغذيات في الوقت الفعلي (وسائل التواصل الاجتماعي، الإشعارات)
// ✓ الأداء حاسم
// ✗ لا يمكن القفز إلى صفحات عشوائية
$posts = Post::latest()->cursorPaginate(20);
// ترقيم الصفحات البسيط - استخدم عندما:
// ✓ تحتاج فقط إلى التنقل "التالي/السابق"
// ✓ تريد أداءً أفضل من ترقيم الإزاحة
// ✓ لا تحتاج إلى إجمالي العدد
$comments = Comment::simplePaginate(20);
تنسيق استجابة ترقيم الصفحات المخصص
تحويل هيكل ترقيم الصفحات الافتراضي في Laravel لمطابقة اتفاقيات API الخاصة بك:
<?php
// app/Http/Resources/PostCollection.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PostCollection extends ResourceCollection
{
public function toArray($request): array
{
return [
'items' => PostResource::collection($this->collection),
'pagination' => [
'total' => $this->total(),
'count' => $this->count(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'total_pages' => $this->lastPage(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
}
// Controller
public function index(): JsonResponse
{
$posts = Post::with('author')->paginate(15);
return response()->json(new PostCollection($posts));
}
</pre>
هيكل استجابة مخصص:
{
"items": [
{"id": 1, "title": "المنشور الأول"},
{"id": 2, "title": "المنشور الثاني"}
],
"pagination": {
"total": 150,
"count": 15,
"per_page": 15,
"current_page": 1,
"total_pages": 10
},
"links": {
"first": "http://api.example.com/posts?page=1",
"last": "http://api.example.com/posts?page=10",
"prev": null,
"next": "http://api.example.com/posts?page=2"
}
}
ترقيم الصفحات مع التصفية والترتيب
دمج ترقيم الصفحات مع معلمات الاستعلام لاسترجاع البيانات القوية:
<?php
public function index(Request $request): JsonResponse
{
$request->validate([
'per_page' => 'integer|min:1|max:100',
'sort_by' => 'in:created_at,title,views',
'sort_direction' => 'in:asc,desc',
'status' => 'in:draft,published,archived',
'search' => 'string|max:255',
]);
$query = Post::with('author:id,name');
// التصفية حسب الحالة
if ($request->has('status')) {
$query->where('status', $request->status);
}
// البحث
if ($request->filled('search')) {
$query->where('title', 'like', '%' . $request->search . '%');
}
// الترتيب
$sortBy = $request->input('sort_by', 'created_at');
$sortDirection = $request->input('sort_direction', 'desc');
$query->orderBy($sortBy, $sortDirection);
// ترقيم الصفحات
$perPage = min($request->input('per_page', 15), 100);
$posts = $query->paginate($perPage);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?status=published&sort_by=views&sort_direction=desc&per_page=25
// GET /api/posts?search=Laravel&page=2
</pre>
تحسين أداء ترقيم الصفحات
1. تجنب مشاكل استعلامات N+1 بالتحميل المسبق
<?php
// سيء: مشكلة استعلام N+1
$posts = Post::paginate(15); // كل منشور يطلق استعلام المؤلف
// جيد: التحميل المسبق
$posts = Post::with('author:id,name', 'tags:id,name')
->paginate(15);
</pre>
2. استخدم Select لتحديد الأعمدة
<?php
// سيء: تحديد جميع الأعمدة بما في ذلك حقول النص الكبيرة
$posts = Post::paginate(15);
// جيد: حدد فقط الأعمدة المطلوبة
$posts = Post::select(['id', 'title', 'excerpt', 'author_id', 'created_at'])
->with('author:id,name')
->paginate(15);
</pre>
3. أضف فهارس قاعدة البيانات
<?php
// Migration
Schema::table('posts', function (Blueprint $table) {
// فهرس لاستعلامات ترقيم الصفحات
$table->index(['created_at', 'id']); // فهرس مركب
// فهرس لترقيم الصفحات المفلتر
$table->index('status');
$table->index(['status', 'created_at']);
// فهرس لترقيم مؤشر
$table->index(['id', 'created_at']);
});
</pre>
تحذير: لترقيم الإزاحة على مجموعات البيانات الكبيرة (page=1000)، يجب على MySQL تخطي جميع الصفوف السابقة، مما يجعل ترقيم الصفحات العميق بطيئًا للغاية. استخدم ترقيم المؤشر لأداء أفضل على مجموعات البيانات الكبيرة.
تفاصيل تنفيذ ترقيم المؤشر
فهم كيفية عمل ترقيم المؤشر تحت الغطاء:
<?php
// ترقيم مؤشر أساسي
$posts = Post::orderBy('id')->cursorPaginate(15);
// مؤشر مع أعمدة ترتيب متعددة
$posts = Post::orderBy('created_at', 'desc')
->orderBy('id', 'desc') // كاسر التعادل للترتيب الفريد
->cursorPaginate(15);
// ترقيم المؤشر مع التصفية (آمن من التناقضات)
$posts = Post::where('status', 'published')
->orderBy('created_at', 'desc')
->orderBy('id', 'desc')
->cursorPaginate(15);
// SQL المولد يستخدم جمل WHERE بدلاً من OFFSET:
// SELECT * FROM posts
// WHERE (created_at < '2024-01-15' OR (created_at = '2024-01-15' AND id < 123))
// ORDER BY created_at DESC, id DESC
// LIMIT 15
</pre>
أفضل ممارسات ترقيم المؤشر: قم دائمًا بتضمين عمود فريد (عادةً id) ككاسر تعادل في جملة orderBy() الخاصة بك لضمان نتائج متسقة.
إلحاق معلمات الاستعلام بروابط ترقيم الصفحات
حفظ معلمات الاستعلام عبر طلبات ترقيم الصفحات:
<?php
public function index(Request $request): JsonResponse
{
$posts = Post::where('status', $request->status)
->paginate(15)
->appends($request->except('page')); // حفظ جميع المعلمات باستثناء 'page'
return response()->json($posts);
}
// بديل: إلحاق معلمات محددة
$posts = Post::paginate(15)->appends([
'status' => $request->status,
'sort_by' => $request->sort_by,
]);
// النتيجة: الروابط تتضمن معلمات الاستعلام
// http://api.example.com/posts?status=published&page=2
</pre>
طرق البيانات الوصفية لترقيم الصفحات
يوفر مرقم الصفحات في Laravel العديد من الطرق المفيدة:
<?php
$posts = Post::paginate(15);
// الحصول على معلومات ترقيم الصفحات
$posts->total(); // إجمالي عدد العناصر
$posts->count(); // عدد العناصر في الصفحة الحالية
$posts->perPage(); // العناصر لكل صفحة
$posts->currentPage(); // رقم الصفحة الحالية
$posts->lastPage(); // رقم الصفحة الأخيرة
$posts->hasPages(); // تحقق مما إذا كان ترقيم الصفحات مطلوبًا
$posts->hasMorePages(); // تحقق مما إذا كانت هناك صفحات أكثر
$posts->onFirstPage(); // تحقق مما إذا كنت في الصفحة الأولى
$posts->onLastPage(); // تحقق مما إذا كنت في الصفحة الأخيرة
$posts->firstItem(); // فهرس العنصر الأول (مثل 16 في الصفحة 2)
$posts->lastItem(); // فهرس العنصر الأخير (مثل 30 في الصفحة 2)
// URLs
$posts->url(5); // URL للصفحة 5
$posts->previousPageUrl(); // URL الصفحة السابقة (null إذا كنت في الأولى)
$posts->nextPageUrl(); // URL الصفحة التالية (null إذا كنت في الأخيرة)
// الحصول على العناصر
$posts->items(); // الحصول على العناصر كمصفوفة
</pre>
ترقيم الصفحات اليدوي
إنشاء ترقيم الصفحات من المصفوفات أو الاستعلامات المخصصة:
<?php
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
// مرقم يدوي مع إجمالي العدد
$items = collect(range(1, 100)); // مصدر البيانات الخاص بك
$perPage = 15;
$currentPage = LengthAwarePaginator::resolveCurrentPage();
$currentItems = $items->slice(($currentPage - 1) * $perPage, $perPage)->values();
$paginator = new LengthAwarePaginator(
$currentItems,
$items->count(),
$perPage,
$currentPage,
['path' => LengthAwarePaginator::resolveCurrentPath()]
);
// مرقم بسيط يدوي (بدون إجمالي العدد)
$simplePaginator = new Paginator(
$currentItems,
$perPage,
$currentPage,
['path' => Paginator::resolveCurrentPath()]
);
</pre>
اختبار ترقيم الصفحات
اكتب اختبارات شاملة لنقاط النهاية المرقمة الخاصة بك:
<?php
// tests/Feature/PostPaginationTest.php
namespace Tests\Feature;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostPaginationTest extends TestCase
{
use RefreshDatabase;
public function test_pagination_returns_correct_number_of_items()
{
Post::factory()->count(50)->create();
$response = $this->getJson('/api/posts?per_page=15');
$response->assertOk()
->assertJsonCount(15, 'data')
->assertJsonStructure([
'current_page',
'data',
'first_page_url',
'from',
'last_page',
'last_page_url',
'next_page_url',
'per_page',
'prev_page_url',
'to',
'total',
]);
$this->assertEquals(50, $response->json('total'));
$this->assertEquals(4, $response->json('last_page'));
}
public function test_pagination_respects_max_per_page()
{
Post::factory()->count(200)->create();
$response = $this->getJson('/api/posts?per_page=999');
$response->assertOk();
$this->assertLessThanOrEqual(100, $response->json('per_page'));
}
public function test_cursor_pagination_works()
{
Post::factory()->count(30)->create();
$response = $this->getJson('/api/posts/cursor');
$response->assertOk()
->assertJsonStructure([
'data',
'next_cursor',
'next_page_url',
'per_page',
]);
// اتبع المؤشر إلى الصفحة التالية
$nextCursor = $response->json('next_cursor');
$response2 = $this->getJson('/api/posts/cursor?cursor=' . $nextCursor);
$response2->assertOk();
}
}
</pre>
تمرين عملي:
- نفذ جميع طرق ترقيم الصفحات الثلاثة (الإزاحة، المؤشر، البسيط) لنقطة نهاية User API مع التصفية حسب الدور والبحث حسب الاسم
- أنشئ تنسيق استجابة API مخصص يطابق مواصفات JSON:API لترقيم الصفحات
- قم ببناء اختبار مقارنة أداء يقيس وقت تنفيذ الاستعلام لترقيم الإزاحة مقابل ترقيم المؤشر في أعماق صفحات مختلفة (صفحة 1، 100، 1000)
- نفذ نظام "ترقيم keyset" باستخدام أعمدة متعددة (created_at + id) للترتيب المتسق حتى عند إدراج سجلات جديدة
- أنشئ مكونًا في React أو Vue ينفذ التمرير اللانهائي باستخدام ترقيم المؤشر مع Laravel API الخاص بك
أفضل الممارسات
أفضل ممارسات ترقيم الصفحات:
- اختر الطريقة الصحيحة: استخدم المؤشر لمجموعات البيانات الكبيرة والتغذيات في الوقت الفعلي، والإزاحة لترقيم الصفحات التقليدي
- حدد القيم الافتراضية المعقولة: 15-25 عنصرًا لكل صفحة مثالي عادةً
- فرض حدود قصوى: منع الإساءة عن طريق تحديد per_page عند 100
- أضف فهارس مناسبة: فهرس الأعمدة المستخدمة في جمل ORDER BY و WHERE
- استخدم التحميل المسبق: تجنب استعلامات N+1 باستخدام with()
- حفظ معلمات الاستعلام: استخدم appends() للحفاظ على الفلاتر عبر الصفحات
- وثق ترقيم الصفحات: اشرح ترقيم الصفحات بوضوح في وثائق API الخاصة بك
- اختبر الحالات الحدية: اختبر النتائج الفارغة، صفحة واحدة، الصفحة الأخيرة، أرقام الصفحات غير الصالحة
الملخص
في هذا الدرس، أتقنت ترقيم صفحات API في Laravel. أنت الآن تفهم الفروق بين ترقيم الإزاحة، وترقيم المؤشر، وترقيم الصفحات البسيط، ومتى تستخدم كل طريقة. لقد تعلمت كيفية تخصيص استجابات ترقيم الصفحات، ودمج ترقيم الصفحات مع التصفية والترتيب، وتحسين الأداء بالفهارس والتحميل المسبق، واختبار نقاط النهاية المرقمة بدقة. يعد ترقيم الصفحات المناسب أساسيًا لبناء واجهات برمجة تطبيقات قابلة للتطوير يمكنها التعامل بكفاءة مع مجموعات البيانات الكبيرة مع توفير تجارب مستخدم ممتازة.