عند بناء 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 بسيطًا ومنظمًا.

جدول المحتويات

  1. لماذا نحتاج Pagination؟
  2. استخدام paginate()
  3. التنقل بين الصفحات
  4. تحديد عدد العناصر
  5. Pagination مع API Resource
  6. شكل JSON Response
  7. ما هي links؟
  8. ما هي meta؟
  9. simplePaginate()
  10. cursorPaginate()
  11. الفرق بين أنواع Pagination
  12. السماح للـClient بتحديد per_page
  13. تحديد Maximum per_page
  14. Validation للـPagination
  15. Pagination مع Filtering
  16. Pagination مع Search
  17. Pagination مع Sorting
  18. الحفاظ على Query String
  19. تخصيص Pagination Response
  20. Pagination والعلاقات
  21. الأداء
  22. مثال متكامل
  23. أفضل الممارسات
  24. ملخص الدرس

لماذا نحتاج 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=3

Laravel يقرأ قيمة 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 منظمة.

ما هي 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 مع 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 Resources

Pagination وتحسين أداء 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=1

Controller

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 سريع ومستقر حتى مع زيادة حجم البيانات.