I recently published a series of articles about creating a custom pagination view where users can enter the page number in a text box. On the second post I received a comment asking how to do pretty URLs with Laravel's paginator. Well here we go, and I will say that there is probably a better way to do this, and probably a package to accomplish it. Proceed with caution.
#The Goal
When use the paginator in Laravel, we get URLs like this:
1http://localhost:8000/users?page=1
and we want URLs like this
1http://localhost:8000/users/page/1
because why not? When digging through the code for the paginator class, I found that the query string method was baked in. So there really is no out-of-the-box way to enable pretty URLs right now. That doesn't mean we can't extend the paginator class, and override the methods we want though.
#Extending Laravel's Paginator
The class we want to extend is Illuminate\Paginator\Paginator
. It has a function named getUrl($page)
; this is responsible for creating the URLs that the paginator uses when rendering the HTML links. Because of this fact, we should figure out a way to override this function and make it generate pretty URLs.
This is the code for the getUrl($page)
function:
1/**
2 * Get a URL for a given page number.
3 *
4 * @param int $page
5 * @return string
6 */
7 public function getUrl($page)
8 {
9 $parameters = array(
10 $this->factory->getPageName() => $page,
11 );
12
13 // If we have any extra query string key / value pairs that need to be added
14 // onto the URL, we will put them in query string form and then attach it
15 // to the URL. This allows for extra information like sorting storage.
16 if (count($this->query) > 0)
17 {
18 $parameters = array_merge($this->query, $parameters);
19 }
20
21 $fragment = $this->buildFragment();
22
23 return $this->factory->getCurrentUrl().'?'.http_build_query($parameters, null, '&').$fragment;
24}
We are going to need to make a few changes to it. Let's start listing the changes we will need to make:
- For starters, we will have to change the
$parameters
variable declaration. We do not need to set thepage
value here anymore. If we do, we will still get the?page=
at the end of the URL. - We will need to change how the current URL is generated. For this we, will probably need to store it in its own variable. This will also force us to change the last
return
line. - We should also add a conditional check on the
$parameters
variable. If there are no parameters, we shouldn't build a query string. Otherwise, we will always have?
added at the end of the URL. Not a big problem, but it still looks weird. - We also need a name for our new paginator. Let's call it
PrettyPaginator
.
#Creating the PrettyPaginator
Class
Create a new file named PrettyPaginator.php
somewhere where Laravel and Composer can find it. For this tutorial, I will just be declaring it in the global namespace. Also, make sure to import (or use
) Laravel's Paginator
class:
1<?php
2
3use Illuminate\Pagination\Paginator;
4
5class PrettyPaginator extends Paginator {
6
7}
That's the start of our class. Remember we need to override the getUrl($page)
function though. So let's add that:
1<?php
2
3use Illuminate\Pagination\Paginator;
4
5class PrettyPaginator extends Paginator {
6
7 /**
8 * Get a URL for a given page number.
9 *
10 * @param int $page
11 * @return string
12 */
13 public function getUrl($page)
14 {
15 // I am empty right now.
16 }
17
18}
For the rest of this section, you can assume that all the code we write will appear in the getUrl($page)
function. Each part will build of the previous part, and we will work through our little check-list we made above. I will also show the new function in its entirety at the end of the section.
#Modifying the $parameters
Declaration
We need to change the $parameters
declaration and make it so it is just an empty array. Let's also store the page name in its own variable, since we will use it later. We do not need to change the code that builds the parameters array from the original implementation of getUrl($page)
, so we can include that too:
1...
2// Holds the paginator page name.
3$pageName = $this->factory->getPageName();
4
5// An array to hold our parameters.
6$parameters = [];
7
8// If we have any extra query string key / value pairs that need to be added
9// onto the URL, we will put them in query string form and then attach it
10// to the URL. This allows for extra information like sortings storage.
11if (count($this->query) > 0)
12{
13 $parameters = array_merge($this->query, $parameters);
14}
15...
#Storing the Current Page's URL
No we need to create a variable to hold our current page URL:
1...
2$currentUrl = $this->factory->getCurrentUrl();
3...
And then we can format our page URL to look pretty:
1...
2$pageUrl = $currentUrl.'/'.$pageName.'/'.$page;
3...
What the above code will do is take the current page URL, for example http://localhost:8000/users
, followed by a forward slash, the pagination page name (usually page
), another forward slash and finally the current page number. So we will get something that looks like this:
1http://localhost:8000/users/page/1
Note: That above piece of code is an example. Do not add it to the
getUrl($page)
function.
#Adding Our Conditional Check
Now we need to add that conditional check we talked about earlier:
1...
2if (count($parameters) > 0)
3{
4 $pageUrl .= '?'.http_build_query($parameters, null, '&');
5}
6
7$pageUrl .= $this->buildFragment();
8...
And then finally we return the current page's URL:
1...
2return $pageUrl;
3...
That's the changes we need to make to the getUrl($page)
function. There are still some things we need to do to make it useful. When we use Laravel's Paginator::make()
function, we are calling the make()
method on a factory class, which just returns back a paginator instance we can use. I really don't want to modify Laravel's service providers in this post, so let's create a static method on our PrettyPaginator
that will accomplish the same thing.
#Creating Our Own make()
Method
We are going to need to resolve some things out of the IoC, so make sure to use
the Illuminate\Support\Facades\App
facade. In our PrettyPaginator
class, add the following function:
1...
2
3/**
4 * Get a new pretty paginator instance.
5 *
6 * @param array $items
7 * @param int $total
8 * @param int|null $perPage
9 * @return \PrettyPaginator
10 */
11public static function make(array $items, $total, $perPage = null)
12{
13 // This is just a static method that will return a paginator class
14 // similar to the default Laravel `Paginator::make()`. Throwing this
15 // in its own static method is easier for now explaining service
16 // providers and such.
17
18 $paginator = new PrettyPaginator(App::make('paginator'), $items, $total, $perPage);
19
20 return $paginator->setupPaginationContext();
21}
22...
its kind of scary, but I'll explain it. When we call the PrettyPaginator::make()
function, we pass it the items we want to paginate, the total number of items, how many results should be on each page. Just like normal. Inside the function, we create a new instance of the PrettyPaginator
class. Remember how we extended the Paginator
class? The Paginator
class needs an instance of Illuminate\Pagination\Factory
as its first argument. Instead of creating one ourselves, we are asking the IoC to make one for us using the App::make('paginator')
function call. And finally we return our new paginator instance after calling the setupPaginationContext()
function, which just calculates some things to use internally.
As promised, here is the full code for our PrettyPaginator
1<?php
2use Illuminate\Support\Facades\App;
3use Illuminate\Pagination\Paginator;
4
5class PrettyPaginator extends Paginator {
6
7 /**
8 * Get a new pretty paginator instance.
9 *
10 * @param array $items
11 * @param int $total
12 * @param int|null $perPage
13 * @return \PrettyPaginator
14 */
15 public static function make(array $items, $total, $perPage = null)
16 {
17 // This is just a static method that will return a paginator class
18 // similar to the default Laravel `Paginator::make()`. Throwing this
19 // in its own static method is easier for now explaining service
20 // providers and such.
21
22 $paginator = new PrettyPaginator(App::make('paginator'), $items, $total, $perPage);
23
24 return $paginator->setupPaginationContext();
25 }
26
27 /**
28 * Get a URL for a given page number.
29 *
30 * @param int $page
31 * @return string
32 */
33 public function getUrl($page)
34 {
35 // Holds the paginator page name.
36 $pageName = $this->factory->getPageName();
37
38 // An array to hold our parameters.
39 $parameters = [];
40
41 // If we have any extra query string key / value pairs that need to be added
42 // onto the URL, we will put them in query string form and then attach it
43 // to the URL. This allows for extra information like sorting storage.
44 if (count($this->query) > 0)
45 {
46 $parameters = array_merge($this->query, $parameters);
47 }
48
49 $currentUrl = $this->factory->getCurrentUrl();
50
51 $pageUrl = $currentUrl.'/'.$pageName.'/'.$page;
52
53 if (count($parameters) > 0)
54 {
55 $pageUrl .= '?'.http_build_query($parameters, null, '&');
56 }
57
58 $pageUrl .= $this->buildFragment();
59
60 return $pageUrl;
61 }
62
63}
#Using the PrettyPaginator
Now that we have the PrettyPaginator
class written, we actually need to use it. This will involve some more work, but its pretty simple. Here are the things we will need to do:
- Create a new Route for our pagination URL,
- Create a function that uses our new paginator,
- Tell Laravel's pagination factory about some changes we want it to use.
#Creating the Route
We are going to paginate a list of users. We want to display users with the URL format: http://localhost:8000/users/page/{page}
. We are going to use a function called getShowResults
in our UsersController
. We can express this in our routes.php
file like this:
1<?php
2...
3Route::get('users/'.Paginator::getPageName().'/{page}', 'UsersController@getShowResults');
4...
The Paginator::getPageName()
will just add the default pagination page name in our route, which is page
by default. The {page}
part is a placeholder and tells Laravel to expect a parameter and to pass it into our controller's function.
#The Controller Function
Now if we move over to our controller, we actually need to implement the getShowResults()
function.
1<?php
2
3...
4
5class UsersController extends BaseController {
6
7 public function getShowResults($page)
8 {
9 // Code here to get a list of users.
10
11 Paginator::setBaseUrl('http://localhost:8000/users');
12 Paginator::setCurrentPage($page);
13 $users = PrettyPaginator::make($pagedData, $totalUsers, $perPage);
14
15 return View::make('users.list')->with('users', $users);
16 }
17
18}
Important: It should be noted that the above code assumes you have building the paginated collections manually.
$pagedData
would be the current page's results,$totalUsers
would be the total number of records, and$perPage
would be how many records should be shown on each page.
Phillip Brown has an excellent article on Culttt (opens in a new window) about implementing custom pagination methods in Laravel 4.
In the above code, we need to tell Laravel's pagination factory what the base URL for the pagination links is. In my case it was http://localhost:8000/users
, and this will have to be adjusted as needed. Setting this will prevent URLs from being generated that look like this http://localhost:8000/users/page/2/page3
. We also need to tell it what the current page is, so it can apply the active classes in the HTML correctly (since it is looking for the URL parameter page
by default). We do this by calling Paginator::setCurrentPage($page)
.
There are no changes that we need to make in our views, so that's good.
#Conclusion
And that's it (well, there was a lot to it, but we are done now)! I am fairly positive there might be a package around that does this, or that there might be an easier way to do this. Now we have our own home-grown way of generating pretty pagination URLs with Laravel.
∎