Advanced Pagination (1.2)

By Rob Conner aka "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 Sat, Jun 30th 2007, 15:13 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 Mon, Jul 9th 2007, 05:57 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 Mon, Jul 16th 2007, 02:59 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 Sun, Aug 19th 2007, 10:07 by Howard Glynn

Comment

5 advanced pagination

removed this comment as I was talking nonsense, sorry!
posted Sun, Aug 19th 2007, 10:48 by Howard Glynn

Question

6 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 Sun, Aug 26th 2007, 03:53 by NDT

Comment

7 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 Tue, Nov 13th 2007, 07:25 by Marc Christenfeldt

Question

8 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 Mon, Dec 17th 2007, 05:56 by Hannibal Lecter

Question

9 Searching incomplete

Great tutorial thanks.

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

posted Fri, Jan 18th 2008, 13:35 by Erico Franco

Question

10 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 Fri, Feb 1st 2008, 22:08 by riki pribadi

Comment

11 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 Tue, Feb 26th 2008, 08:00 by Sohaib Muneer

Login to Submit a Comment