Advanced Pagination (1.2)

By Rob Conner (rtconner)
This tutorial will attempt to cover some advanced techniques of pagination. In large this will cover Ajax pagination. Hopefully we can also uncover some of the better practices and techniques to use with pagination.
Please be sure you are familiar with the basics of Cake pagination. This information can be found in the basic CakePHP pagination tutorial.

This tutorial is going to be a work in progress though. There are likely some "best practices" I am not familiar with. I would like to compile a fairly good amount of "best practices" into this tutorial. I am sure the many smart people in the Cake community will help out as they find opportunity to give input.

Ok let's get started. First we will go through an Ajax pagination example. Then after that there will just be a discussion of abstract ideas on pagination techniques.

Ajax Pagination

In reality CakePHP makes pagination with Ajax fairly simple. I'm going to just work with the Customer list from the basic tutorial. Again, your Model does not need to do anything special. So we'll just look at the View and Controller code you will need to implement.

First things first, make sure prototype.js is included by your layout. You can't do any Ajax without the javascript Ajax library. All of my layouts usually looks like this.
Download code <?php if(!empty($ajax)): ?>
    <?php echo $javascript->link('prototype');?>
<?php 
endif; ?>

You'll have to get your own ajax loader, but your view can look like this.

View Template:

Download code This is the customer listing page. There are many things I might like to put here. Below is a list of all of the customers in the database.
<br /><br />

<div id="LoadingDiv" style="display: none;">
    <?php echo $html->image('ajax-loader.gif'); ?>
</div>

<div id="CustomerPaging">
    <?php echo $this->renderElement('customers/paging'); ?>
</div>

Now the actual paging is happening within an element, you need to make an element. I put my elements within a folder of the name of the controller that uses it. This one is elements/customers/paging.ctp.
Download code <?php
    $paginator
->options(
            array(
'update'=>'CustomerPaging'
                    
'url'=>array('controller'=>'Customers''action'=>'display'), 
                    
'indicator' => 'LoadingDiv'));
?>

Showing Page <?php echo $paginator->counter(); ?>
<table>
    <tr>
        <th><?php echo $paginator->sort('Name''name');?></th>
        <th><?php echo $paginator->sort('Store''store');?></th>
    </tr>
<?php foreach($customers as $customer): ?>
    <tr>
        <td><?php echo $customer['Customer']['name']; ?></td>
        <td><?php echo $customer['Customer']['store']; ?></td>
    </tr>
<?php endforeach; ?>
</table>
<?php echo $paginator->prev(); ?> -
<?php echo $paginator->numbers(array('separator'=>' - ')); ?>
<?php 
echo $paginator->next('Next Page'); ?>

The controller now needs to know when its just a page load, and when it an Ajax call. We use Cake's RequestHandler to accomplish this.

Controller Class:

Download code <?php 
class CustomersController extends Controller {

    var 
$name 'Customers';

    var 
$components = array('RequestHandler');
    
    var 
$paginate = array('limit' => 15'page' => 1'order'=>array('name'=>'asc'));

    function 
display() {
        if(!
$this->RequestHandler->isAjax()) {
            
// things you want to do on initial page load go here
            
$this->pageTitle "Customer List";    
        }
        
        
$this->set('customers'$this->paginate('Customer'));
        
        if(
$this->RequestHandler->isAjax()) {
            
$this->viewPath 'elements'.DS.'customers';
            
$this->render('paging');            
        }
    }
}
?>

Searching

A very common use for pagination is probably search pages. Lets try to find a good technique we can use to implement a paginated search.

The view is going to change just a little. We'll add a form and a text field for the user to enter a search term into.

View Template:

Download code <?php echo $form->create('Customer', array('action'=>'display'))?>

<?php echo $form->text('Customer.search'); ?>
<div id="CustomerPaging">
    <?php echo $this->renderElement('customers/paging'); ?>
</div>

</form>

Now the controller is where things get a little tricky. The paginator won't hold on to the search term for page 2 or page 3 and so forth. So we have to hold onto the search term ourselves and pass it along to the paginator manually. I have used sessions to implement this functionality. Here is how my controller looks.

Controller Class:

Download code <?php 
...
function 
display() {
    if(!
$this->RequestHandler->isAjax()) {
        
$this->pageTitle "Customer List";    
        
// clear the session on first page visit
        
$this->Session->del($this->name.'.search');
    }
    
    if(!empty(
$this->data))
        
$search $this->data['Customer']['search'];
    elseif(
$this->Session->check($this->name.'.search'))
        
$search $this->Session->read($this->name.'.search');

    
$filters = array();
    if(isset(
$search)) {
        
$filters = array("lower(Customer.name) like '%".low($search)."%'");
        
$this->Session->write($this->name.'.search'$search);        
    }
        
    
$this->set('customers'$this->paginate('Customer'$filters));
    
    if(
$this->RequestHandler->isAjax()) {
        
$this->viewPath 'elements'.DS.'customers';
        
$this->render('paging');            
    }
}

...
?>

Other Techniques

Of course there are other things you might want to with pagination. Here are a few other little techniques I've picked up as I've gone along.

Action parameters1
Lets say you have an action that looks like this..
Download code <?php
...
function 
display($one=null$two=null) {
    if(!(
$one && $two))
        
$this->cakeError('error404', array($this->params['url']));
...
?>
You can get the paginator to hold onto parameters $one and $two by using 'url'=>$this->params['pass'] in your options array. Possibly you might want to make it standard to do something along this lines in your views:
Download code <?php
    $paginator
->options(
            array(
'update'=>'CustomerPaging'
                    
'url'=>$this->params['pass'],
                    
'model'=>'Customer'
                    
'indicator' => 'LoadingDiv'));
?>
Empty Pagination2
There will likey be a sitution where it would be convenient to get an empty pagination set. That can be accomplished using something like this:
Download code <?php
    $this
->set('customers'$this->paginate('Customer', array('id'=>null)));
?>

Known Weaknesses

Cake 1.2 is still in development. I feel obligated to inform the reader of known weaknesses in pagination, before they dive in and start using it. There are only two things I know of which have been any hinderance to anybody.

  1. Sorting by another model - Perhaps your model you want to paginate with has a belongsTo relationship to another model. You may want to sort by that other model. Currently this is not possible for security reasons. I'm told it will be done before Cake 1.2 becomes official. For now though, I am sorry you can only sort by the Model you are paginating with.
  2. Paginating with Javascript - Perhaps you want to move to page two of your list or sort using some sort of javascript command. Unfortunaltely this is difficult to do right now because the paginator only returns full anchor tags. This is not super hard to hack and get around. Below is a little function I've used to extract the url from a link. Download code <?php $url preg_replace('/<a\s+.*?href="([^"]+)"[^>]*>([^<]+)<\/a>/is''\1'$link); ?>

Credits
Quick thanks to 1Andy Dawson (AD7six), Jitka Koukalova (poLK), and 2Jared Hoyt. The sum of mutiple brains is better than mine is.

 

Comments 448

CakePHP Team Comments Author Comments
 

Question

1 What about cake 1.1.x

What if I want to use this functionality in cake 1.1.x, I already have a website which uses cake 1.1.15. Is it possible to use the pagination features from cake 1.2 in cake 1.1 or will I have to upgrade cake and code as well?
Posted Jun 30, 2007 by Alejandro Lopez Hernandez
 

Comment

2 Ordering for multiple tables

Hi Rob, great writeup. You mention that ordering over multiple tables is not yet possible but it actually is (or it has become possible in the latest version). To use this functionality make sure your base model contains the other table, setting recursion to a higher level usually helps.

In your controller, first set the ordering for your pagination:


$this->paginate = array('order'=>array('Table2.somefield'=>'asc'));

And then make sure you create the pagination object with a high enough setting for recursive so the second table is included in the result.


$myPaginateObject = $this->paginate('Table1', array(), array('recursive'=>2));

Then you should be able to use the paginate object in your view without a hitch.
Posted Jul 9, 2007 by Tijs Teulings
 

Comment

3 Sorting by another model

I can confirm that sorting by another model's fields is supported :) but there is a little bug - when clicking on a sorting link that is specified as Model.field instead of field the direction seems to be always asc for the links. i must investigate if its a bug or is the is a problem with my setup :)
Posted Jul 16, 2007 by Marcin Domanski
 

Comment

4 Sorting by another model

Another confirmation that it is possible to sort by another model. (1.2.0.5427)

For the purposes of managing a long list of subcategories (being paginated!), i use bindModel() to temporarily make category belongTo subcategory. Category is the parent, but whilst managing the subcategory list I need access to the name of the parent for display and html select etc

So in my controller index method:

Controller Class:

<?php 
$this
->Subcategory->bindModel(
 array(
'belongsTo' => array(
  
'Category' => array(
   
'foreignKey' => 'category_id'
   'fields' 
=> 'categorylabel'
  
)
)), 
false);
?>

Note carefully the use of "false" so that the binding does not reset. When left at default (i.e. true) I don't get the additional Category data. When explicitly set to false, you do get the required data. I didn't look at the src code but I suspect that more than one call using the Subcategory model is being made, and the binding is reset after that, so when the next call comes along, you don't get your data. Hope that helps someone, there is a lot of noise on the support alias around this topic and it appears the answer is quite straightforward!

Before posting this I double checked my model/cleared cache etc and it was completely clean (no belongsTo/hasMany etc in there) so I know this is activating the required behaviour. I didn't even set recursion explicitly at any point.


Posted Aug 19, 2007 by Howard Glynn
 

Question

5 How to make a Pagination Ajax with mootools

I have a problem when use mootools instead of prototype to make a ajax site with cakePHP! I can't make pagination ajax with mootools, can you help me with an example?

Thanks all!

Mootools: http://mootools.net
Posted Aug 26, 2007 by NDT
 

Comment

6 Sorting By Another Model

As Marcin Domanski said, there seems to be a bug with sorting by another model, as the sorting order is always ASC. I'm not so much into the cake code to find out whether this is a real bug, but as a workaround I copied paginator.php to my app/views/helpers directory and changed the line

return preg_replace('/.*\./', '', key($options['order']));
in the function sortKey to

return preg_replace( sprintf( '/%s\./', $this->defaultModel()), '', key($options['order']));
Worked fine for me.

Cheers
Posted Nov 13, 2007 by Marc Christenfeldt
 

Question

7 paginating child model

Hi,

is there a way to paginate a "child" model?

For example, I have an Article model and Comment model. Of course, Article hasMany Comments. I want to display an Article and his comments paginated below (possibly with Ajax).

Is this doable? I'm working with latest 1.2 nightly.
Posted Dec 17, 2007 by Hannibal Lecter
 

Question

8 Searching incomplete

Great tutorial thanks.

Searching view is broken? Looks it is missing at least a submit button or I missing something?

Posted Jan 18, 2008 by Erico Franco
 

Question

9 its not working

hi everyone...
let say that i have 3 tables,
1. category = (id,name)
2. subcategory = (id,category_id,name)
3. subsubcategory = (id,subcategory_id,name)

then, in controller subsubcategory:index

Controller Class:

<?php 
function index() 
{
        
$this->SubSubCategory->recursive 2;

        
$this->SubSubCategory->bindModel(
                                         array(
'belongsTo' => array(
                                          
'SubCategory' => array(
                                           
'foreignKey' => 'subcategory_id'
                                          
)
                                        )), 
false);
        
$this->SubSubCategory->SubCategory->bindModel(
                                         array(
'belongsTo' => array(
                                          
'Category' => array(
                                           
'foreignKey' => 'category_id'
                                          
)
                                        )), 
false);
        
$this->set('data'$this->paginate('SubSubCategory'null, array('recursive'=>2)));
}
?>

and sorting still not working...anyone can help me ???
Posted Feb 1, 2008 by riki pribadi
 

Comment

10 simple but powerful workaround

hi everyone,

I was facing problems too with the pagination because I wanted to "unbind" all the associated models before paginating. When I briefly analyzed Controller::paginate a simple work around struck me. Using this you can define your own custom pagination functions different for different models.
So all you've got to do is copy / paste this function in your app_controller.php (I hope you keep one in the app dir) and make 2 small changes.

1. Change the signature from:

paginate($object = null, $scope = array(), $whitelist = array())
to:

paginate($object = null, $scope = array(), $whitelist = array(), $customMethod = null)

2. Replace this block within the function:


if (method_exists($object, 'paginate')) {
    $results = $object->paginate($conditions, $fields, $order, $limit, $page, $recursive);
} else {
    $results = $object->findAll($conditions, $fields, $order, $limit, $page, $recursive);
}

with:


if ($customMethod) {
    $results = $object->{$customMethod}($conditions, $fields, $order, $limit, $page, $recursive);
} else {
    if (method_exists($object, 'paginate')) {
        $results = $object->paginate($conditions, $fields, $order, $limit, $page, $recursive);
    } else {
        $results = $object->findAll($conditions, $fields, $order, $limit, $page, $recursive);
    }
}

Usage:

for example, if you have a 'User' model, you can write User::customPaginate and pass 'customPaginate' as string for $customMethod variable.

BE CAUTIOUS!
This might not be the best of the solutions but it's certainly working great for me. Be cautious that whatever 'customMethod' you define in your model should return a 'findAll' results array. You can bind / unbind / search inner models your way {go wild!}
Posted Feb 26, 2008 by Sohaib Muneer
 

Comment

11 2 different way to paging

can i use two different way to paing with paginator helper in the same controller. May i redefine the variable $paginator?
Posted Jun 23, 2008 by yunhaihuang
 

Comment

12 Results per page

How to display results per page option in view? Paginator supports this feature?
Posted Jun 26, 2008 by Mihai Copae
 

Question

13 Change var pagination

1. It's posible change var: "page" in pagination
2. Change separator: ":"
Posted Jun 30, 2008 by Howar Rasguido
 

Bug

14 Pagination delete session variables

I'd implemented pagination on my project, but there's problem. When i moved through pages (example: from page 3 to 9), sometimes(or usually) my session variables get rid off. They were gone and my shopping cart got empty or i lost my login session. I dont know what cause this problem.

Any idea? Can Anyone help? Thanks u.

Sorry for my English.
Posted Jul 16, 2008 by Alfa Ryano
 

Comment

15 how did you tested?

I'd implemented pagination on my project, but there's problem. When i moved through pages (example: from page 3 to 9), sometimes(or usually) my session variables get rid off. They were gone and my shopping cart got empty or i lost my login session. I dont know what cause this problem.

Any idea? Can Anyone help? Thanks u.

Sorry for my English.

How do you know is related to Paginator? I have been experiencing Session variables random deletes sinces I've been using 1.2 and I havent been able to discover any recurrent behavior. Could you explain that a little bit more?
Posted Oct 28, 2008 by Daniel Bernal
 

Comment

16 Custom parameters in $paginator->sort()

Great tutorial and very helpful, just like the paginator code docs itself.

Just a small question:

Controller Class:

<?php 
function loadFixtures($sport_id$competition_id) {
$this->set('fixtures'$this->paginate(null"Fixture.sport_id = '$sport_id' AND Fixture.competition_id = '$competition_id'"));
$this->set('sport_id'$sport_id);
$this->set('competition_id'$competition_id);
$this->render('load_fixtures''ajax');
}
?>

The initial view renders, but the sort fails, because the parameters $sport_id, $competition_id cannot be passed through $paginator->sort();

Is there anyway to change the sort url, and add custom parameters to filter on?
Posted Dec 9, 2008 by jarrett
 

Comment

17 @jarrett

$this->set('fixtures', $this->paginate(null, array("Fixture.sport_id = '$sport_id' AND Fixture.competition_id = '$competition_id'")))
The second parameter needs to be a conditions array, not a string.
Posted Feb 19, 2009 by Rob Conner
 

Question

18 Multiple Pagination of same model on same page - help please :)

I have to have one page that has 3 tables on it. Each of the tables needs to be paginated, and they are ALL from the SAME model. They are for Trades, so I'm having 3 tables, one will show all trades in state open, the other for all trades in state closed, and the final all in pending state.

My question is, how can I call the paginate 3 different times in the same model? I do:

$this->set('trades1', $this->paginate('Trade', array('Trade.trading_state_id'=>'open')));
$this->set('trades2', $this->paginate('Trade', array('Trade.trading_state_id'=>'closed')));
$this->set('trades3', $this->paginate('Trade', array('Trade.trading_state_id'=>'pending')));

Then when I render the screen, I use the appropriate data for the appropriate table. HOWEVER, all of the paging stats (next, previous, # of records) are all the data from the last paginate call of course...

I'm using AJAX to render only the appropriate section, although I don't know where the user is coming from, it is re-rendering the entire screen (all 3 tables) in the desired output div instead of just the one.

In summary, how can you call paginate multiple times for the same model in your controller? Also, is there a way of knowing where you came from (ie which table did you click next on) from your views?

Hope this makes sense. Any help would be appreciated.
Posted Feb 23, 2009 by Tara Page
 

Comment

19 change var "page" to something else for paginator

1. It's posible change var: "page" in pagination
2. Change separator: ":"

hi there,

well i managed to change variable "page" to something else (phrase in my language) for pagination component but it`s quite tricky. this is what i did (Cake 1.2):

1) copy paginate() method from /cake/libs/controller/controller.php to /app/app_controller.php

find this line:

$options = array_merge($this->params, $this->params['url'], $this->passedArgs);

and add following code above:

if (isset($this->passedArgs['yourpagephrase'])) {
$this->passedArgs['page'] = $this->passedArgs['yourpagephrase'];
}

2) copy /cake/libs/view/helpers/paginator.php to /app/views/helpers/paginator.php

find this line inside link() method:

return $this->{$obj}->link($title, Set::filter($url, true), $options);

and add following code above:

$url['yourpagephrase'] = $url['page'];
//filter out array
$url = array_diff_key( $url, array_flip( array('page') ) );

Posted Mar 14, 2009 by misieg
 

Comment

20 change var "page" to something else for paginator

1. It's posible change var: "page" in pagination
2. Change separator: ":"
I have tried as you mentioned by copying the helper and the paginate function. But I was confused how to do these changes.

I have tried changing the var like this---
$this->paginate = array('limit' => 5, 'featured' => 1,'order'=>array('User.id' => 'desc'));
$this->set('featured',$this->paginate('User',array('User.is_featured=1')));

$this->paginate = array('limit' => 5, 'non_featured' => 1,'order'=>array('User.id' => 'desc'));
$this->set('non_featured',$this->paginate('User',array('User.is_featured="0"')));

This doesn't work for me. Is there any known issue for this?
Any help would be appreciated. Thanks a ton!!!
Posted Jun 13, 2009 by php developer
 

Comment

21 Great tutorial

Thanks for this great tutorial, I was able to adapt it to my project and it's work fine in ajax with jQuery instead of prototype.
Posted Jun 17, 2009 by Jean-Philippe Sirois