LaravelAPIPaginationMahir4 mnt baca

Pagination: Data Besar per Halaman

Bagi hasil query jadi halaman dengan paginate() dan simplePaginate().

Kenapa Tidak Boleh load Semua Data

Post::all() di tabel berisi 2 juta baris adalah cara tercepat membunuh server: memori PHP habis, response JSON ratusan MB, database terkunci lama. Pagination memotong hasil menjadi halaman-halaman kecil yang bisa dicerna. Tapi tidak semua pagination sama, memilih jenis yang salah di data besar sama buruknya dengan tidak paginasi sama sekali.

Tiga Jenis Pagination

php
// app/Http/Controllers/PostController.php

// 1. paginate(): lengkap dengan total halaman
//    menjalankan 2 query: SELECT COUNT(*) + SELECT ... LIMIT 10 OFFSET 20
$posts = Post::latest()->paginate(10);

// 2. simplePaginate(): tanpa total, hanya tahu ada/tidak halaman berikut
//    1 query: SELECT ... LIMIT 11 (1 baris ekstra untuk deteksi next page)
$posts = Post::latest()->simplePaginate(10);

// 3. cursorPaginate(): tanpa offset, pakai WHERE id > last_id
//    1 query, konsisten walau data berubah saat user paging
$posts = Post::latest()->cursorPaginate(10);

paginate() butuh COUNT(*) untuk menghitung total halaman. Di tabel jutaan baris, COUNT bisa lebih lambat dari query datanya sendiri. simplePaginate() cocok untuk feed infinite scroll yang tidak butuh nomor halaman ("Muat lagi"). cursorPaginate() adalah yang paling scalable: ia tidak memakai OFFSET sama sekali, jadi halaman 10.000 secepat halaman 1, dan tidak ada data duplikat atau hilang kalau ada baris baru masuk saat user sedang paging.

Pagination di Blade

php
// controller
$posts = Post::with('user')->latest()->paginate(8);

// resources/views/posts/index.blade.php
@foreach ($posts as $post)
    <h2>{{ $post->title }}</h2>
@endforeach

{{ $posts->links() }} {{-- tombol halaman otomatis, styling Tailwind --}}

Kustomisasi penting saat ada filter: tanpa ini, pindah halaman menghilangkan filter yang sedang aktif.

php
$posts = Post::when($request->kategori, function ($query, $kategori) {
        $query->where('category_id', $kategori);
    })
    ->latest()
    ->paginate(8)
    ->withQueryString(); // pertahankan ?kategori=X di link halaman

withQueryString() menambahkan semua query string saat ini ke URL halaman, jadi /katalog?kategori=3&page=2 tetap memfilter kategori 3. Alternatif yang lebih eksplisit: ->appends(['kategori' => $request->kategori]).

Pagination di API

php
// app/Http/Controllers/Api/PostController.php
public function index(Request $request)
{
    $perPage = (int) $request->input('per_page', 10);
    $perPage = max(1, min($perPage, 50)); // clamp: 1 sampai 50

    return PostResource::collection(
        Post::with('user')->latest()->paginate($perPage)
    );
}

Response JSON-nya terstruktur otomatis:

json
{
    "data": [ /* ... */ ],
    "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
    "meta": {
        "current_page": 1, "last_page": 42,
        "per_page": 10, "total": 417,
        "from": 1, "to": 10
    }
}

Clamp per_page itu wajib. Tanpa batas atas, client bisa meminta ?per_page=1000000 dan pagination-mu jadi pajangan. Angka 50 adalah batas wajar untuk kebanyakan kasus.

Kursor untuk Data Real-Time

Untuk feed yang datanya berubah cepat (timeline, notifikasi), cursor pagination lebih tepat:

php
$posts = Post::orderBy('id')->cursorPaginate(10);

// response meta berisi cursor, bukan nomor halaman:
// "next_page_url": "/api/posts?cursor=eyJpZCI6MTAs..."

Syaratnya: harus ada orderBy pada kolom unik (biasanya primary key). Tanpa ordering yang deterministik, cursor tidak bisa menentukan "setelah baris ini".

Jebakan Umum

Pertama, OFFSET besar itu lambat. paginate() halaman 50.000 berarti database memindai dan membuang 500.000 baris sebelum mengambil 10. Kalau user bisa melompat ke halaman sembarang di data besar, batasi halaman maksimum atau pindah ke cursor.

Kedua, paginate() di dalam Resource collection yang di-map() manual. PostResource::collection($posts->map(...)) merusak objek paginator dan meta hilang. Transformasi per item yang custom pakai ->through() pada paginator:

php
$posts = Post::paginate(10)->through(fn ($post) => [
    'id' => $post->id,
    'judul' => str($post->title)->upper(),
]);

Ketiga, lupa eager load di query paginasi. Pagination tidak menyelamatkanmu dari N+1: 10 item per halaman dengan relasi lazy berarti 11 query per halaman. with() tetap wajib.

Catatan teknis: Aturan pemilihannya sederhana: butuh nomor halaman dan total (admin table, katalog)? paginate(). Infinite scroll tanpa total? simplePaginate(). Data besar atau berubah cepat? cursorPaginate(). Dan berapa pun jenisnya, selalu clamp per_page dari user.

Tantangan

Halaman Katalog

Buat halaman /katalog yang menampilkan 8 produk per halaman dengan $products->links(). Tambahkan filter kategori via query string dan pastikan withQueryString() menjaga filter saat pindah halaman.