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.
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.
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 added14 // onto the URL, we will put them in query string form and then attach it15 // 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:
$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.return
line.$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.PrettyPaginator
.PrettyPaginator
ClassCreate 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<?php2 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 $page11 * @return string12 */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.
$parameters
DeclarationWe 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 it10// 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...
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.
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.
make()
MethodWe 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 \PrettyPaginator10 */11public static function make(array $items, $total, $perPage = null)12{13 // This is just a static method that will return a paginator class14 // similar to the default Laravel `Paginator::make()`. Throwing this15 // in its own static method is easier for now explaining service16 // 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 $items11 * @param int $total12 * @param int|null $perPage13 * @return \PrettyPaginator14 */15 public static function make(array $items, $total, $perPage = null)16 {17 // This is just a static method that will return a paginator class18 // similar to the default Laravel `Paginator::make()`. Throwing this19 // in its own static method is easier for now explaining service20 // 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 $page31 * @return string32 */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 added42 // onto the URL, we will put them in query string form and then attach it43 // 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}
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:
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<?php2...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.
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.
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.
∎
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.