Pagination is a common feature in web applications. Almost every Laravel application I've ever worked on has had some form of pagination implemented.
But what is pagination and why do we use it? How can we implement pagination in our Laravel applications? And how do we decide which pagination method to use?
In this article, we're going to answer those very questions and explore how to use pagination in Laravel for both Blade views and API endpoints. By the end of this article, you should feel confident enough to start using pagination in your own projects.
What is Pagination?
Pagination is a technique used to divide a large dataset into smaller chunks (or pages). It allows you to display a subset of the data, rather than all the possible values at once.
For instance, imagine you had a page that outputs the names of all the users in your application. If you had thousands of users, it wouldn't be practical to display them all on a single page. Instead, you could use pagination to display a subset of the users (say 10 users at a time) on each page, and allow users to navigate between the pages to view more users (the next 10).
By using pagination you can:
- Improve the performance of your application - Since you're fetching a smaller subset of data at a time, there's less data for you to fetch from the database, process/transform, and then return.
- Improve the user experience - It's likely that the user will only ever be interested in a small subset of the data at a time (typically found in the first few pages, especially if filters and search terms are used). By using pagination, you can avoid displaying data that the user is not interested in.
- Improve page loading times - By only fetching a subset of the data at a time, you can reduce the amount of data that needs to be loaded onto the page, which can improve page loading and JavaScript processing times.
Pagination can typically be split into two different types:
- Offset-based pagination - This is the most common type of pagination you'll likely come across in your web apps, especially in user interfaces (UI). It involves fetching a subset of data from the database based on an "offset" and a "limit". For example, you might fetch 10 records starting from the 20th record to fetch the 3rd page of data.
- Cursor-based pagination - This type of pagination involves fetching a subset of data based on a "cursor". The cursor is typically a unique identifier for a record in the database. For example, you might fetch the next 10 records starting from the record with an ID of 20.
Laravel provides three different methods for paginating Eloquent queries in your applications:
paginate
- Uses offset-based pagination and fetches the total number of records in the dataset.simplePaginate
- Uses offset-based pagination but doesn't fetch the total number of records in the dataset.cursorPaginate
- Uses cursor-based pagination and doesn't fetch the total number of records in the dataset.
Let's take a look at each of these methods in more detail.
Using the paginate
Method
The paginate
method allows you to fetch a subset of
data from the database based on an offset and limit (we'll take a
look at these later when we look at the underlying SQL
queries).
You can use the paginate
method like so:
use App\Models\User;
$users = User::query()->paginate();
Running the above code would result in the $users
being an instance of
Illuminate\Contracts\Pagination\LengthAwarePaginator
,
typically an
Illuminate\Pagination\LengthAwarePaginator
object.
This paginator instance contains all the information you need to
display the paginated data in your application.
The paginate
method can automatically determine the
requested page number based on the page
query
parameter in the URL. For example, if you visited
https://my-app.com/users?page=2
, the
paginate
method would fetch the second page of
data.
By default, all the pagination methods in Laravel default to fetching 15 records at a time. However, this can be changed to a different value (we'll take a look at how to do this later).
Using paginate
with Blade Views
Let's take a look at how to use the paginate
method
when rendering data in a Blade view.
Imagine we have a simple route that fetches the users from the database in a paginated format and passes them to a view:
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('users', function () {
$users = User::query()->paginate();
return view('users.index', [
'users' => $users,
]);
});
Our resources/views/users/index.blade.php
file
might look something like this:
<html>
<head>
<title>Paginate</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Paginate</h1>
<ul class="py-4">
@foreach ($users as $user)
<li class="py-1 border-b">{{ $user->name }}</li>
@endforeach
</ul>
{{ $users->links() }}
</div>
</body>
</html>
The resulting page would look something like this:
Let's break down what's happening in the Blade view:
- We're looping through each user that is present in the
$users
field (theIlluminate\Pagination\LengthAwarePaginator
object) and outputting their name. - We're calling the
links
method on the$users
object. This is a really handy method which returns some HTML that displays the pagination links (e.g., "Previous", "Next", and the page numbers). This means you don't have to worry about creating the pagination links yourself, and Laravel will handle all of that for you.
We can also see that the paginate
method is giving
us an overview of the pagination data. We can see that we're
viewing the 16th to 30th records, out of a total of 50 records. We
can also see that we're on the second page and that there are a
total of 4 pages.
It's important to note that the links
method will
return the HTML styled using Tailwind CSS. If you wish to use
something other than Tailwind or you want to style the pagination
links yourself, you can check out the documentation on customizing pagination views.
Using paginate
in API Endpoints
As well as using the paginate
method in Blade
views, you can also use it in API endpoints. Laravel makes this
process easy by automatically converting the paginated data into
JSON.
For instance, we could build an /api/users
endpoint
(by adding the following route to our routes/api.php
file) which returns the paginated users in JSON format:
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('paginate', function () {
return User::query()->paginate();
});
Accessing the /api/users
endpoint would return a
JSON response similar to the following (please note I've limited
the data
field to just 3 records for the sake of
brevity):
{
"current_page": 1,
"data": [
{
"id": 1,
"name": "Andy Runolfsson",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 2,
"name": "Rafael Cummings",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 3,
"name": "Reynold Lindgren",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
}
],
"first_page_url": "https://example.com/users?page=1",
"from": 1,
"last_page": 4,
"last_page_url": "https://example.com/users?page=4",
"links": [
{
"url": null,
"label": "« Previous",
"active": false
},
{
"url": "https://example.com/users?page=1",
"label": "1",
"active": true
},
{
"url": "https://example.com/users?page=2",
"label": "2",
"active": false
},
{
"url": "https://example.com/users?page=3",
"label": "3",
"active": false
},
{
"url": "https://example.com/users?page=4",
"label": "4",
"active": false
},
{
"url": "https://example.com/users?page=5",
"label": "5",
"active": false
},
{
"url": "https://example.com/users?page=2",
"label": "Next »",
"active": false
}
],
"next_page_url": "https://example.com/users?page=2",
"path": "https://example.com/users",
"per_page": 15,
"prev_page_url": null,
"to": 15,
"total": 50
}
Let's break down the JSON response:
current_page
- The current page we're on. In this case, we're on the first page.data
- The actual data itself that's being returned. In this case, it contains the first 15 users (shortened to 3 for brevity).first_page_url
- The URL to the first page of data.from
- The starting record number of the data being returned. In this case, it's the first record. If we were on the second page, this would be 16.last_page
- The total number of pages of data. In this case, there are 4 pages.last_page_url
- The URL to the last page of data.links
- An array of links to the different pages of data. This includes the "Previous" and "Next" links, as well as the page numbers.next_page_url
- The URL to the next page of data.path
- The base URL of the endpoint.per_page
- The number of records being returned per page. In this case, it's 15.prev_page_url
- The URL to the previous page of data. In this case, it'snull
because we're on the first page. If we were on the second page, this would be the URL to the first page.to
- The ending record number of the data being returned. In this case, it's the 15th record. If we were on the second page, this would be 30.total
- The total number of records in the dataset. In this case, there are 50 records.
The Underlying SQL Queries
Using the paginate
method in Laravel results in two
SQL queries being run:
- The first query fetches the total number of records in the dataset. This is used to determine information such as the total number of pages and the total number of records.
- The second query fetches the subset of data based on the offset and limit values. For example, it might be fetching the users for us to process and return.
So if we wanted to fetch the first page of users (with 15 users per page), the following SQL queries would be run:
select count(*) as aggregate from `users`
and
select * from `users` limit 15 offset 0
In the second query, we can see that the limit
value is set to 15. This is the number of records that are returned
per page.
The offset
value is calculated as follows:
Offset = Page size * (Page - 1)
So if we wanted to fetch the third page of users, the
offset
value would be calculated as:
Offset = 15 * (3 - 1)
Therefore, the offset
value would be 30 and we
would fetch the 31st to 45th records. The queries for the third
page would look like so:
select count(*) as aggregate from `users`
and
select * from `users` limit 15 offset 30
Using the simplePaginate
Method
The simplePaginate
method is very similar to the
paginate
method but with one key difference. The
simplePaginate
method doesn't fetch the total number
of records in the dataset.
As we've just seen, when we use the paginate
method, we also get information about the total number of records
and pages available in the dataset. We can then use this
information for displaying things like the total number of pages in
the UI or API response.
But if you do not intend to display these details to the user
(or developer consuming the API), then we can avoid an unneeded
database query (that counts the total number of records) by using
the simplePaginate
method.
The simplePaginate
method can be used in the same
way as the paginate
method:
use App\Models\User;
$users = User::query()->simplePaginate();
Running the above code would result in the $users
being an instance of
Illuminate\Contracts\Pagination\Paginator
, typically
an Illuminate\Pagination\Paginator
object.
Unlike the
Illuminate\Pagination\LengthAwarePaginator
object
returned by the paginate
method, the
Illuminate\Pagination\Paginator
object doesn't contain
information about the total number of records in the dataset and
has no idea how many pages or total records there are. It just
knows about the current page of data and whether there are more
records to fetch.
Using simplePaginate
with Blade Views
Let's take a look at how you can use the
simplePaginate
method with a Blade view. We'll assume
we have the same route as before, but this time we're using the
simplePaginate
method:
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('users', function () {
$users = User::query()->simplePaginate();
return view('users.index', [
'users' => $users,
]);
});
We'll build our Blade view in the same way as before:
<html>
<head>
<title>Simple Paginate</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Simple Paginate</h1>
<ul class="py-4">
@foreach ($users as $user)
<li class="py-1 border-b">{{ $user->name }}</li>
@endforeach
</ul>
{{ $users->links() }}
</div>
</body>
</html>
The resulting page would look something like this:
As we can see in this example, the output of
$users->links()
is different to the output we saw
when using the paginate
method. Since the
simplePaginate
method doesn't fetch the total number
of records, it has no context of the total number of pages or
records, only whether there's a next page or not. Therefore, we
only see the "Previous" and "Next" links in the pagination
links.
Using simplePaginate
in API Endpoints
You can also use the simplePaginate
method in API
endpoints. Laravel will automatically convert the paginated data
into JSON for you.
Let's build an /api/users
endpoint that returns the
paginated users in JSON format:
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('users', function () {
return User::query()->simplePaginate();
});
When we hit this route, we'll get a JSON response similar to the
following (I've limited the data
field to just 3
records for brevity):
{
"current_page": 1,
"data": [
{
"id": 1,
"name": "Andy Runolfsson",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 2,
"name": "Rafael Cummings",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 3,
"name": "Reynold Lindgren",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
}
],
"first_page_url": "https://example.com/users?page=1",
"from": 1,
"next_page_url": "https://example.com/users?page=2",
"path": "https://example.com/users",
"per_page": 15,
"prev_page_url": null,
"to": 15
}
As we can see, the JSON response is very similar to the response
we got when using the paginate
method. The key
difference is that we don't have the last_page
,
last_page_url
, links
, or
total
fields in the response.
The Underlying SQL Queries
Let's take a look at the underlying SQL queries that are run
when using the simplePaginate
method.
The simplePaginate
method still relies on the
limit
and offset
values to fetch the
subset of data from the database. However, it doesn't run the query
to fetch the total number of records in the dataset.
The offset
value is still calculated in the same
way as before:
Offset = Page size * (Page - 1)
However, the limit
value is calculated slightly
differently than the paginate
method. It's calculated
as:
Limit = Page size + 1
This is because the simplePaginate
method needs to
fetch one more record than the perPage
value to
determine if there are more records to fetch. For example, let's
say we're fetching 15 records per page. The limit
value would be 16. So if 16 records were to be returned, we'd know
there is at least one more page of data available to fetch. If any
less than 16 records were returned, we'd know that we're on the
last page of data.
So if we wanted to fetch the first page of users (with 15 users per page), the following SQL queries would be run:
select * from `users` limit 16 offset 0
The query for the second page would look like so:
select * from `users` limit 16 offset 15
Using the cursorPaginate
Method
So far we've looked at the paginate
and
simplePaginate
methods which both use offset-based
pagination. We're now going to take a look at the
cursorPaginate
method which uses cursor-based
pagination.
As a heads-up, cursor-based pagination might seem a little confusing the first time you come across it. So don't worry if you don't quite get it straight away. Hopefully, by the end of this article, you'll have a better understanding of how it works. I'll also leave an awesome video at the end of this article that explains cursor-based pagination in more detail.
With offset-based pagination, we use the limit
and
offset
values to fetch a subset of data from the
database. So we can say "skip the first 10 records and fetch the
next 10 records". This is simple to understand and easy to
implement. Whereas with cursor pagination, we use a cursor
(typically a unique identifier for a specific record in the
database) as a starting point to fetch the previous/next set of
records.
For example, let's say we make a query to fetch the first 15 users. We'll assume the ID of the 15th user is 20. When we want to fetch the next 15 users, we'll use the ID of the 15th user (20) as the cursor. We'll say "fetch the next 15 users with an ID greater than 20".
You may sometimes see cursors referred to as "tokens", "keys", "next", "previous", and so on. They're essentially a reference to a specific record in the database. We'll look at the structure of the cursors later in this section when we take a look at the underlying SQL queries.
Laravel allows us to easily use cursor-based pagination with the
cursorPaginate
method:
use App\Models\User;
$users = User::query()->cursorPaginate();
Running the above code would result in the $users
field being an instance of
Illuminate\Contracts\Pagination\CursorPaginator
,
typically an Illuminate\Pagination\CursorPaginator
object. This paginator instance contains all the information you
need to display the paginated data in your application.
Similar to the simplePaginate
method, the
cursorPaginate
method doesn't fetch the total number
of records in the dataset. It only knows about the current page of
data and whether there are more records to fetch, so we're not
immediately aware of the total number of pages or records.
Using cursorPaginate
with Blade Views
Let's take a look at how to use the cursorPaginate
method when rendering data in a Blade view. Similar to our previous
examples, we'll assume we have a simple route that fetches the
users from the database in a paginated format and passes them to a
view:
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('users', function () {
$users = User::query()->cursorPaginate();
return view('users.index', [
'users' => $users,
]);
});
The Blade view might look something like this:
<html>
<head>
<title>Cursor Paginate</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="max-w-5xl mx-auto py-8">
<h1 class="text-5xl">Cursor Paginate</h1>
<ul class="py-4">
@foreach ($users as $user)
<li class="py-1 border-b">{{ $user->name }}</li>
@endforeach
</ul>
{{ $users->links() }}
</div>
</body>
</html>
This would output a page similar to the following:
As we can see, since the cursorPaginate
method
doesn't fetch the total number of records in the dataset, the
output of $users->links()
is similar to the output
we saw when using the simplePaginate
method. We only
see the "Previous" and "Next" links in the pagination links.
Using cursorPaginate
in API Endpoints
Laravel also allows you to use the cursorPaginate
method in API endpoints and will automatically convert the
paginated data into JSON for us.
Let's build an /api/users
endpoint that returns the
paginated users in JSON format:
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('users', function () {
return User::query()->cursorPaginate();
});
When we hit this route, we'll get a JSON response similar to the
following (I've limited the data
field to just 3
records for brevity):
{
"data": [
{
"id": 1,
"name": "Andy Runolfsson",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 2,
"name": "Rafael Cummings",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 3,
"name": "Reynold Lindgren",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
}
],
"path": "https://example.com/users",
"per_page": 15,
"next_cursor": "eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"next_page_url": "https://example.com/users?cursor=eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"prev_cursor": null,
"prev_page_url": null
}
As we can see, the JSON response is similar to the previous
responses we've seen but with some small differences. Since we
aren't fetching the total number of records, we don't have the
last_page
, last_page_url
,
links
, or total
fields in the response.
You may also have noticed we don't have the from
and
to
fields either.
Instead, we have the next_cursor
and
prev_cursor
fields which contain the cursor for the
next and previous pages of data. Since we're on the first page, the
prev_cursor
and prev_page_url
fields are
both null
. However, the next_cursor
and
next_page_url
fields are set.
The next_cursor
field is a base-64 encoded string
that contains the cursor for the next page of data. If we decode
the next_cursor
field, we'd get something like this
(beautified for readability):
{
"users.id": 15,
"_pointsToNextItems": true
}
The cursor contains two separate pieces of information:
users.id
- The ID of the last record fetched in the dataset._pointsToNextItems
- A boolean value that tells us whether the cursor points to the next or previous set of items. If the value istrue
it means the cursor should be used to fetch the next set of records with an ID greater than theusers.id
value. If the value isfalse
, it means the cursor should be used to fetch the previous set of records with an ID less than theusers.id
value.
Let's take a look at what the second page of data might look like (again, shortened to 3 records for brevity):
{
"data": [
{
"id": 16,
"name": "Durward Nikolaus",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 17,
"name": "Dr. Glenda Cruickshank IV",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
},
{
"id": 18,
"name": "Prof. Dolores Predovic",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"email_verified_at": "2024-10-15T23:19:28.000000Z",
"created_at": "2024-10-15T23:19:29.000000Z",
"updated_at": "2024-10-15T23:19:29.000000Z"
}
],
"path": "https://example.com/users",
"per_page": 15,
"next_cursor": "eyJ1c2Vycy5pZCI6MzAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"next_page_url": "https://example.com/users?cursor=eyJ1c2Vycy5pZCI6MzAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"prev_cursor": "eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9",
"prev_page_url": "https://example.com/users?cursor=eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9"
}
We can see that the prev_cursor
and
prev_page_url
fields are now set, and the
next_cursor
and next_page_url
fields have
been updated with the cursor for the next page of data.
The Underlying SQL Queries
To get a better understanding of how the cursor pagination works
under the hood, let's take a look at the underlying SQL queries
that are run when using the cursorPaginate
method.
On the first page of data (containing 15 records), the following SQL query would be run:
select * from `users` order by `users`.`id` asc limit 16
We can see that we're fetching the first 16 records from the
users
table and ordering them by the id
column in ascending order. Similar to the
simplePaginate
method, we're fetching 16 rows because
we want to determine if there are more records to fetch.
Let's imagine we then navigate to the next page of items with the following cursor:
eyJ1c2Vycy5pZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0
When this cursor is decoded, we get the following JSON object:
{
"users.id": 15,
"_pointsToNextItems": true
}
Laravel will then run the following SQL query to fetch the next set of records:
select * from `users` where (`users`.`id` > 15) order by `users`.`id` asc limit 16
As we can see, we're fetching the next 16 records from the
users
table that have an id
larger than
15 (since 15 was the last ID on the previous page).
Now let's assume that the ID of the first user on page 2 is 16. When we navigate back to the first page of data from the second page, the following cursor would be used:
eyJ1c2Vycy5pZCI6MTYsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9
When this is decoded, we get the following JSON object:
{
"users.id": 16,
"_pointsToNextItems": false
}
When we're moving to the next page of results, the last record
fetched is used as the cursor. When we move back to the previous
page of results, the first record fetched is used as the cursor.
For this reason, we can see the users.id
value is set
to 16 in the cursor. We can also see that the
_pointsToNextItems
value is set to false
because we're moving back to the previous set of items.
As a result, the following SQL query would be run to fetch the previous set of records:
select * from `users` where (`users`.`id` < 16) order by `users`.`id` desc limit 16
As we can see, the where
constraint is now checking
for records with an id
less than 16 (since 16 was the
first ID on page 2) and the results are ordered in descending
order.
Using API Resources with Pagination
So far, in our API examples, we've just returned the paginated data directly from the controller. However, in a real-world application, you'll likely want to process the data before returning it to the user. This could be anything from adding or removing fields, converting data types, or even transforming the data into a different format altogether. For this reason, you'll likely want to use API Resources since they provide a way for you to consistently transform your data before returning it.
Laravel allows you to use API resources alongside pagination. Let's look at an example of how to do this.
Imagine we have created an
App\Http\Resources\UserResource
API resource class
that transforms the user data before returning it. It might look
something like this:
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}
In the toArray
method, we're defining that whenever
we process a user via this resource, we only want to return the
id
, name
, and email
fields.
Now let's build a simple /api/users
API endpoint in
our routes/api.php
file that returns the paginated
users using the App\Http\Resources\UserResource
:
use App\Models\User;
use App\Http\Resources\UserResource;
use Illuminate\Support\Facades\Route;
Route::get('users', function () {
$users = User::query()->paginate();
return UserResource::collection(resource: $users);
});
In the code above, we're fetching a single page of users (let's
assume it's the first page containing 15 users) from the database.
We're then passing the $users
field (which will be an
instance of
Illuminate\Pagination\LengthAwarePaginator
) to the
UserResource::collection
method. This method will
transform the paginated data using the
App\Http\Resources\UserResource
before returning it to
the user.
When we hit the /api/users
endpoint, we'll get a
JSON response similar to the following (I've limited the
data
field to just 3 records for brevity):
{
"data": [
{
"id": 1,
"name": "Andy Runolfsson",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. "
},
{
"id": 2,
"name": "Rafael Cummings",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. "
},
{
"id": 3,
"name": "Reynold Lindgren",
"email": "This email address is being protected from spambots. You need JavaScript enabled to view it. "
}
],
"links": {
"first": "https://example.com/users?page=1",
"last": "https://example.com/users?page=4",
"prev": null,
"next": "https://example.com/users?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 4,
"links": [
{
"url": null,
"label": "« Previous",
"active": false
},
{
"url": "https://example.com/users?page=1",
"label": "1",
"active": true
},
{
"url": "https://example.com/users?page=2",
"label": "2",
"active": false
},
{
"url": "https://example.com/users?page=3",
"label": "3",
"active": false
},
{
"url": "https://example.com/users?page=4",
"label": "4",
"active": false
},
{
"url": "https://example.com/users?page=2",
"label": "Next »",
"active": false
}
],
"path": "https://example.com/users",
"per_page": 15,
"to": 15,
"total": 50
}
}
As we can see in the JSON above, Laravel detects that we're
working with a paginated dataset and returns the paginated data in
a similar format as before. However, this time the users in the
data
field only contain the id
,
name
, and email
fields which we specified
in our API resource class. Other fields (current_page
,
from
, last_page
, links
,
path
, per_page
, to
, and
total
) are still returned as they're part of the
paginated data, but they've been placed inside a meta
field. There's also a links
field that contains the
first
, last
, prev
, and
next
links to the different pages of data.
Changing the Per Page Value
When building views with paginated data, you might want to allow the user to change the number of records displayed per page. This might be via a dropdown or number input field.
Laravel makes it easy to change the number of records displayed
per page by passing a perPage
parameter to the
simplePaginate
, paginate
, and
cursorPaginate
methods. This parameter allows you to
specify the number of records you want to display per page.
Let's take a look at a simple example of how to read a
per_page
query parameter and use this to change the
number of records fetched per page:
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('users', function (Request $request) {
$perPage = $request->integer('per_page', default: 10);
return User::query()->paginate(perPage: $perPage);
});
In the example above, we're grabbing the per_page
query parameter's value. If the value isn't provided, we'll default
to 10. We then pass that value to the perPage
parameter of the paginate
method.
We could then access these different URLs:
https://my-app.com/users
- Display the first page of users with 10 records per page.https://my-app.com/users?per_page=5
- Display the first page of users with 5 records per page.https://my-app.com/users?per_page=5&page=2
- Display the second page of users with 5 records per page.- And so on...
How to Decide Which Pagination Method to Use
Now that we've looked at the different types of pagination and how to use them in Laravel, we'll discuss how to decide which of these approaches to use in your application.
Do You Need the Page Number or the Total Number of Records?
If you're building a UI or API endpoint that requires the total
number of records or pages to be displayed, then the
paginate
method is probably a sensible choice.
If you don't require either of these, then the
simplePaginate
or cursorPaginate
will be
more efficient as they don't perform unnecessary queries to count
the total number of records.
Do You Need to Jump to a Specific Page?
If you need to be able to jump to a specific page of data, then offset-based pagination is more suitable. Since cursor pagination is stateful, it relies on the previous page to know where to go next. So it's not as easy to jump to a specific page.
Whereas when using offset pagination, you can typically just pass the page number in the request (maybe as a query parameter) and jump to that page without having any context of the previous page.
How Large is the Dataset?
Due to the way that databases handle offset
values,
offset-based pagination becomes less efficient as the page number
increases. This is because when you're using an offset, the
database still has to scan through all the records up to the offset
value. They're just discarded and not returned in the query
results.
Here's a great article that explains this in more detail: https://use-the-index-luke.com/no-offset.
So as the total amount of data in the database grows and the page number increases, offset-based pagination can become less efficient. In these cases, cursor-based pagination is more performant, especially if the cursor field is indexed, since the previous records aren't read. For this reason, if you're going to be using pagination against a large dataset, you might want to opt for cursor pagination over offset pagination.
Is the Dataset Likely to Change Often?
Offset-based pagination can suffer from issues if the underlying dataset changes between requests.
Let's take a look at an example.
Let's say we have the following 10 users in our database:
- User 1
- User 2
- User 3
- User 4
- User 5
- User 6
- User 7
- User 8
- User 9
- User 10
We make a request to fetch the first page (containing 5 users) and get the following users:
- User 1
- User 2
- User 3
- User 4
- User 5
When we navigate to page 2, we'd expect to get users 6 to 10. However, let's imagine that before we load page 2 (while we're still viewing page 1), User 1 is deleted from the database. Since the page size is 5, the query to fetch the next page would look like this:
select * from `users` limit 5 offset 5
This means we're skipping the first 5 records and fetching the next 5.
This would result in page 2 containing the following users:
- User 7
- User 8
- User 9
- User 10
As we can see, User 6 is missing from the list. This is because User 6 is now the 5th record in the table, so they're actually on the first page.
Cursor-based pagination doesn't have this issue, because we're not skipping records, we're just fetching the next set of records based on a cursor. Let's imagine we'd used cursor-based pagination in the example above. The cursor for page 2 would be the ID of User 5 (which we'll assume is 5) since it was the last record on the first page. So our query for page 2 may look like this:
select * from `users` where (`users`.`id` > 5) order by `users`.`id` asc limit 6
Running the above query would return users 6 to 10 as expected.
This should hopefully highlight how offset-based pagination can become problematic when the underlying data is changed, added to, or removed while it's being read. It becomes less predictable and can lead to unexpected results.
Are You Building an API?
It's important to remember that you're not fixed to using a single type of pagination in your application. In some places, offset pagination might be more suitable (maybe for UI purposes) and in others, cursor pagination might be more efficient (such as when working with a large dataset). So you can mix and match pagination methods in your application depending on the use case.
However, if you're building an API, I'd highly recommend that you're consistent and use a single pagination approach for all your endpoints. This will make it easier for developers to understand how to use your API and avoid any confusion.
You don't want them to have to remember which endpoints use offset-pagination and which use cursor-pagination.
Of course, this isn't a hard and fast rule. If you really need to use a different pagination method in one particular endpoint, then go ahead. But just make sure to make it clear in the documentation to make it easier for developers to understand.
Prefer a Video Instead?
If you're more of a visual learner, you might want to check out this awesome video by Aaron Francis that explains the difference between offset and cursor-based pagination in more detail:
<iframe width="560" height="315" src="https://www.youtube.com/embed/zwDIN04lIpc" title="Pagination in MySQL - offset vs. cursor" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>Conclusion
In this article, we've taken a look at the different types of pagination in Laravel and how to use them. We've also looked at their underlying SQL queries and how to decide which pagination method to use in your application.
Hopefully, you should now feel more confident in using pagination in your Laravel applications.
The post A Guide to Pagination in Laravel appeared first on Laravel News.
Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.