Query di chapter Membaca Data
sudah bisa kita filter dan urutkan, tetapi satu keputusan masih terbuka: berapa baris yang ditampilkan sekaligus? Menampilkan seluruh isi tabel masih wajar untuk dua puluh baris, tetapi menjadi masalah ketika tabel berisi dua puluh ribu baris — response membengkak, memori server bekerja keras, dan pengguna harus menggulir tanpa arah. Laravel menyediakan paginator bawaan yang mengerjakan tiga hal sekaligus: memotong hasil query per halaman, menghitung jumlah halaman, dan menyiapkan tautan navigasi. Chapter ini membahas paginate(), dua variannya yang lebih cepat, penampilan navigasi di Blade, sampai bentuk JSON-nya untuk API — semuanya menurut dokumentasi pagination
.
Membagi Hasil dengan paginate() #
Kita mulai dari data uji yang jumlahnya pasti: satu user bernama Budi dan dua puluh post bernomor, dibuat lewat model factory :
$budi = User::factory()->create(['name' => 'Budi', 'email' => '[email protected]']);
for ($i = 1; $i <= 20; $i++) {
$n = str_pad((string) $i, 2, '0', STR_PAD_LEFT);
Post::factory()->create([
'user_id' => $budi->id,
'judul' => "Belajar Pagination $n",
'slug' => "belajar-pagination-$n",
]);
}Method paginate() lalu memotong hasil query. Kita minta lima belas baris per halaman, lalu memeriksa objek LengthAwarePaginator yang dikembalikannya:
$posts = Post::paginate(15);
echo 'jumlah item: ', $posts->count(), "\n";
echo 'item pertama: ', $posts->items()[0]->judul, "\n";
echo 'item terakhir: ', $posts->items()[14]->judul, "\n";
echo 'total: ', $posts->total(), "\n";
echo 'perPage: ', $posts->perPage(), "\n";
echo 'currentPage: ', $posts->currentPage(), "\n";
echo 'lastPage: ', $posts->lastPage(), "\n";
echo 'hasPages: ', $posts->hasPages() ? 'true' : 'false', "\n";
echo 'hasMorePages: ', $posts->hasMorePages() ? 'true' : 'false', "\n";
echo 'url(2): ', $posts->url(2), "\n";
echo 'previousPageUrl: ', var_export($posts->previousPageUrl(), true), "\n";Output yang Dihasilkan:
jumlah item: 15
item pertama: Belajar Pagination 01
item terakhir: Belajar Pagination 15
total: 20
perPage: 15
currentPage: 1
lastPage: 2
hasPages: true
hasMorePages: true
url(2): http://localhost:8000?page=2
previousPageUrl: NULLDi balik layar, paginate() menjalankan dua query: satu COUNT(*) untuk mengisi total dan lastPage, satu query LIMIT/OFFSET untuk mengambil lima belas baris halaman aktif. Method items() mengembalikan Collection berisi model-model halaman itu — pola yang sama dengan get(), hanya jumlahnya dibatasi. Karena total diketahui, paginator inilah satu-satunya yang tahu jumlah halaman pasti.
Berpindah Halaman #
Dalam request HTTP, Laravel membaca nomor halaman dari query string ?page= secara otomatis, jadi di controller kita cukup menulis Post::paginate(15). Pada skrip CLI kita mensimulasikan halaman kedua lewat argumen keempat paginate():
$posts = Post::paginate(15, ['*'], 'page', 2);
echo 'jumlah item: ', $posts->count(), "\n";
echo 'currentPage: ', $posts->currentPage(), "\n";
echo 'hasMorePages: ', $posts->hasMorePages() ? 'true' : 'false', "\n";
echo 'previousPageUrl: ', $posts->previousPageUrl(), "\n";
echo 'nextPageUrl: ', var_export($posts->nextPageUrl(), true), "\n";Output yang Dihasilkan:
jumlah item: 5
currentPage: 2
hasMorePages: false
previousPageUrl: http://localhost:8000?page=1
nextPageUrl: NULLHalaman kedua hanya berisi lima post tersisa. Accessor-accessor berikut adalah yang paling sering kita pakai:
| Method | Arti |
|---|---|
total(), perPage(), currentPage(), lastPage() | Angka dasar pagination |
items(), count() | Isi dan jumlah model halaman aktif |
url($n) | URL menuju halaman ke-$n |
previousPageUrl(), nextPageUrl() | Tautan sebelum/sesudah, NULL di ujung |
hasPages(), hasMorePages(), onFirstPage() | Pemeriksaan posisi |
simplePaginate() dan cursorPaginate() #
Query COUNT(*) pada paginate() semakin mahal ketika tabel membesar, padahal tidak semua tampilan butuh angka total. simplePaginate() membuang query hitung itu dan hanya menyisakan tautan sebelum/sesudah:
$simple = Post::simplePaginate(15);
echo 'jumlah item: ', $simple->count(), "\n";
echo 'nextPageUrl: ', $simple->nextPageUrl(), "\n";
echo 'punya method total(): ', method_exists($simple, 'total') ? 'ya' : 'tidak', "\n";
echo 'punya method lastPage(): ', method_exists($simple, 'lastPage') ? 'ya' : 'tidak', "\n";Output yang Dihasilkan:
jumlah item: 15
nextPageUrl: http://localhost:8000?page=2
punya method total(): tidak
punya method lastPage(): tidakVarian tercepat adalah cursorPaginate(). Method ini tidak memakai offset sama sekali, melainkan penunjuk (cursor) posisi terakhir yang dilihat:
$cursor = Post::cursorPaginate(15);
echo 'jumlah item: ', $cursor->count(), "\n";
echo 'hasMorePages: ', $cursor->hasMorePages() ? 'true' : 'false', "\n";
echo 'nextCursor: ', $cursor->nextCursor() ? $cursor->nextCursor()->encode() : 'null', "\n";
echo 'isi kursor (terdecode): ', json_encode($cursor->nextCursor()->toArray()), "\n";Output yang Dihasilkan:
jumlah item: 15
hasMorePages: true
nextCursor: eyJwb3N0cy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0
isi kursor (terdecode): {"posts.id":15,"_pointsToNextItems":true}Cursor ternyata hanyalah JSON kecil berisi id terakhir yang terenkode base64. Halaman berikutnya diminta dengan query string ?cursor=..., dan database hanya menjalankan WHERE id > 15 LIMIT 15 — tanpa membaca ulang baris-baris halaman sebelumnya seperti offset.
| Method | Query COUNT(*) | Tahu total | Tahu nomor halaman | Cocok untuk |
|---|---|---|---|---|
paginate() | ya | ya | ya | daftar admin, hasil pencarian |
simplePaginate() | tidak | tidak | ya (sebelum/sesudah) | feed, katalog umum |
cursorPaginate() | tidak | tidak | tidak (hanya cursor) | tabel besar, infinite scroll |
Navigasi di Blade #
Di sisi view, paginator yang dilempar controller cukup dipanggil method links():
<div>
@foreach ($posts as $post)
<article>{{ $post->judul }}</article>
@endforeach
</div>
{{ $posts->links() }}Method links() merender daftar nomor halaman memakai view bawaan Laravel yang berclass CSS Tailwind — pada proyek ber-Tailwind navigasi langsung tampil rapi tanpa satu baris CSS pun. Proyek ber-Bootstrap cukup memilih view lain, misalnya {{ $posts->links('pagination::bootstrap-5') }}.
Bentuk JSON untuk API #
Bila paginator dikembalikan langsung dari route, Laravel mengubahnya menjadi JSON berisi data halaman plus metadata navigasi. Feature test berikut memverifikasi bentuk JSON tersebut:
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class PaginationJsonTest extends TestCase
{
use RefreshDatabase;
public function test_bentuk_json_respon_paginate(): void
{
$budi = User::factory()->create(['name' => 'Budi', 'email' => '[email protected]']);
for ($i = 1; $i <= 20; $i++) {
$n = str_pad((string) $i, 2, '0', STR_PAD_LEFT);
Post::factory()->create([
'user_id' => $budi->id,
'judul' => "Belajar Pagination $n",
'slug' => "belajar-pagination-$n",
]);
}
Route::get('/api/posts', fn () => Post::select('id', 'judul', 'slug')->paginate(15));
$json = $this->getJson('/api/posts?page=2')->json();
echo 'total: ', $json['total'], "\n";
echo 'per_page: ', $json['per_page'], "\n";
echo 'current_page: ', $json['current_page'], "\n";
echo 'last_page: ', $json['last_page'], "\n";
echo 'from: ', $json['from'], "\n";
echo 'to: ', $json['to'], "\n";
echo 'prev_page_url: ', $json['prev_page_url'], "\n";
echo 'next_page_url: ', var_export($json['next_page_url'], true), "\n";
echo 'data: ', count($json['data']), ' item; item pertama = ',
$json['data'][0]['id'], ' | ', $json['data'][0]['judul'], "\n";
$this->assertSame(20, $json['total']);
$this->assertSame(2, $json['current_page']);
$this->assertSame(5, count($json['data']));
}
}Output yang Dihasilkan:
total: 20
per_page: 15
current_page: 2
last_page: 2
from: 16
to: 20
prev_page_url: http://localhost:8000/api/posts?page=1
next_page_url: NULL
data: 5 item; item pertama = 16 | Belajar Pagination 16Kunci from dan to menunjukkan rentang nomor baris, sedangkan kunci data berisi model halaman aktif. Respons lengkapnya masih menyertakan first_page_url, last_page_url, path, dan links berisi navigasi bernomor. Satu kebutuhan praktis tersisa: parameter filter seperti ?urut=judul harus ikut terbawa di setiap tautan halaman. Method appends() mengerjakannya:
$posts = Post::paginate(15);
echo 'tanpa appends: ', $posts->url(2), "\n";
echo 'dengan appends: ', $posts->appends(['urut' => 'judul'])->url(2), "\n";Output yang Dihasilkan:
tanpa appends: http://localhost:8000?page=2
dengan appends: http://localhost:8000?urut=judul&page=2Tanpa appends(), klik halaman dua akan membuang parameter urut sehingga urutan berubah di tengah jalan. Karena method ini hanya melampirkan query string, bentuk JSON-nya pun otomatis membawa parameter tersebut di setiap URL.
Dengan pagination, daftar data kita tetap ringan berapa pun isi tabelnya. Namun ada masalah lain yang lebih halus: kadang data perlu “dihapus” tanpa benar-benar hilang dari database. Chapter berikutnya membahas soft delete yang menyelesaikan kebutuhan itu.