Laravel Paginator Pretty URLs

July 28, 2014 —John Koster

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 the page 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.

Some absolutely amazing
people

The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.