Nội dung bài viết
- 1.Vì sao cần phân trang?
- 2.Phân trang với paginate()
- 3.Hiển thị dữ liệu và link phân trang
- 4.simplePaginate và cursorPaginate
- 5.Giữ tham số lọc và tìm kiếm trên URL
- 6.Tùy chỉnh giao diện phân trang
- 6.1Dùng Bootstrap
- 6.2Tự thiết kế view phân trang
- 7.Phân trang trả JSON cho API
- 8.Phân trang một mảng hoặc Collection có sẵn
- 9.Kinh nghiệm từ Hozitech
Laravel Pagination là tính năng phân trang có sẵn trong Laravel: chỉ cần gọi ->paginate(15) trên truy vấn Eloquent hoặc Query Builder, Laravel tự thêm LIMIT, OFFSET theo tham số ?page= trên URL, và {{ $posts->links() }} in ra thanh chuyển trang. Không cần tự tính số trang hay viết SQL.
Bài viết cập nhật theo Laravel 13 (bản hiện hành, PHP 8.3+): ba kiểu phân trang, hiển thị link, giữ tham số lọc trên URL, đổi giao diện sang Bootstrap hoặc tự thiết kế, trả JSON cho API và phân trang một mảng có sẵn.
Vì sao cần phân trang?
Bảng có vài nghìn bài viết hay đơn hàng mà tải hết một lần sẽ làm truy vấn chậm, tốn bộ nhớ và trang nặng, người dùng cũng không đọc hết. Phân trang chỉ lấy đúng số bản ghi cần hiển thị, giúp trang nhanh hơn và dễ duyệt hơn.
Phân trang với paginate()
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\View\View;
class PostController extends Controller
{
public function index(): View
{
$posts = Post::query()
->where('status', 'published')
->latest()
->paginate(15);
return view('posts.index', compact('posts'));
}
}
Với Query Builder cũng tương tự: DB::table('posts')->paginate(15). Laravel đọc tham số page trên URL để biết trang hiện tại. Viết controller và truy vấn với model được trình bày trong bài controller trong Laravel và model trong Laravel.
Hiển thị dữ liệu và link phân trang
<!-- resources/views/posts/index.blade.php -->
@foreach ($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
</article>
@endforeach
{{ $posts->links() }}
<p>Hiển thị {{ $posts->firstItem() }}–{{ $posts->lastItem() }} trên tổng {{ $posts->total() }} bài</p>
Đối tượng phân trang duyệt được như mảng và có thêm các phương thức: currentPage(), lastPage(), total(), perPage(), hasPages(), hasMorePages(), nextPageUrl(), previousPageUrl(). Muốn hiện nhiều số trang hơn hai bên trang hiện tại, dùng {{ $posts->onEachSide(2)->links() }}. Cú pháp Blade xem thêm trong bài view trong Laravel.
simplePaginate và cursorPaginate

// Chỉ có nút Trước / Sau, bỏ truy vấn COUNT(*)
$posts = Post::latest()->simplePaginate(15);
// Phân trang theo con trỏ: cần orderBy trên cột duy nhất
$posts = Post::orderBy('id')->cursorPaginate(15);
paginate() phải chạy thêm một truy vấn đếm tổng, với bảng rất lớn có thể chậm. simplePaginate() bỏ bước đếm. cursorPaginate() không dùng OFFSET mà so sánh giá trị cột đã sắp xếp (WHERE id > 15), nên nhanh và ổn định hơn với dữ liệu lớn hoặc thay đổi liên tục; đổi lại không có link số trang và cột sắp xếp không được chứa giá trị null.
Giữ tham số lọc và tìm kiếm trên URL
Trang danh sách có ô tìm kiếm hay bộ lọc (?q=laravel&sort=new) sẽ mất các tham số này khi bấm sang trang 2 nếu không xử lý. Cách gọn nhất là withQueryString():
$posts = Post::query()
->when($request->q, fn ($query, $q) => $query->where('title', 'like', "%{$q}%"))
->latest()
->paginate(15)
->withQueryString();
// Hoặc chỉ nối một số tham số
$posts->appends(['sort' => 'new']);
Đoạn {!! $users->appends(request()->all()->links() !!} trong một số bài hướng dẫn cũ vừa thiếu ngoặc vừa sai (request()->all() trả về mảng); hãy dùng withQueryString() như trên.
Nếu một trang có hai danh sách phân trang, đặt tên tham số riêng cho từng cái: Post::paginate(10, ['*'], 'bai_viet') (tham số thứ ba là tên tham số trang).
Tùy chỉnh giao diện phân trang
Dùng Bootstrap
Mặc định, HTML của link phân trang dùng class Tailwind CSS. Laravel vẫn hỗ trợ Bootstrap; nếu giao diện của bạn dùng Bootstrap, bật trong AppServiceProvider:
use Illuminate\Pagination\Paginator;
public function boot(): void
{
Paginator::useBootstrapFive(); // hoặc useBootstrapFour()
}
Tự thiết kế view phân trang
Xuất các view phân trang của Laravel ra thư mục dự án để sửa:
php artisan vendor:publish --tag=laravel-pagination
Các file được chép vào resources/views/vendor/pagination; tailwind.blade.php là view mặc định. Nếu chạy php artisan vendor:publish không kèm --tag, Artisan hiện danh sách để chọn; số thứ tự của mục laravel-pagination khác nhau giữa các dự án, nên dùng thẳng --tag cho chắc.

Để dùng một view riêng cho mọi trang, khai báo Paginator::defaultView('pagination.custom') trong AppServiceProvider, hoặc truyền tên view cho từng chỗ: {{ $posts->links('pagination.custom') }}.
Phân trang trả JSON cho API
Trả thẳng đối tượng phân trang từ route hoặc controller, Laravel tự chuyển thành JSON gồm data (danh sách bản ghi) và các thông tin current_page, last_page, per_page, total, next_page_url, prev_page_url:
Route::get('/api/posts', function () {
return Post::latest()->paginate(20);
});
Phân trang một mảng hoặc Collection có sẵn
Collection không có phương thức paginate(). Khi dữ liệu đã nằm trong bộ nhớ (ví dụ kết quả gọi API bên ngoài), tự cắt dữ liệu và tạo LengthAwarePaginator:
use Illuminate\Pagination\LengthAwarePaginator;
$items = collect($ketQuaApi);
$perPage = 10;
$page = LengthAwarePaginator::resolveCurrentPage();
$paginator = new LengthAwarePaginator(
$items->forPage($page, $perPage)->values(),
$items->count(),
$perPage,
$page,
['path' => request()->url(), 'query' => request()->query()],
);
Nếu dữ liệu đến từ CSDL, luôn phân trang ngay trong truy vấn thay vì lấy hết rồi cắt. Các phương thức forPage(), values() của Collection được giải thích trong bài Laravel Collection.
Kinh nghiệm từ Hozitech
Trên các website Laravel do Hozitech phát triển, trang danh sách bài viết, sản phẩm thường dùng paginate() kèm withQueryString() để giữ bộ lọc; trang quản trị có bảng dữ liệu lớn như lịch sử đơn hàng thì cân nhắc simplePaginate() hoặc cursorPaginate() để tránh truy vấn đếm chậm. Với trang danh mục cần lên Google, URL phân trang nên giữ dạng ?page=2 rõ ràng và mỗi trang có nội dung riêng.
Nếu website của doanh nghiệp cần tối ưu tốc độ các trang danh sách hoặc bảo trì định kỳ, có thể tham khảo dịch vụ chăm sóc website của Hozitech.
Về tác giả
Founder
Xin chào mình là Hiền hiện đang là Founder của Hozitech. Mình là kỹ sư phần mềm có hơn 9+ năm kinh nghiệm trong lĩnh vực thiết kế website, ứng dụng di động... Gần đây mình đang nghiên cứu thêm về lĩnh vực marketing. Mình rất vui khi được chia sẽ những kiến thức và kinh nghiệm của mình đến mọi người!
Câu hỏi thường gặp