Advanced Pagination (1.2)
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.
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.
You'll have to get your own ajax loader, but your view can look like this.
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.
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.
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.
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.
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.
<?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:
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.
<?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:
<?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:
<?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:
<?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..
<?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:
<?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:
<?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.- [li]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. [li]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.
<?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.

Hi
I've been struggling to find a way to do this as well.... has anyone any ideas please!
"Hi,
I have a problem with pagination. when I click on next, after reloading page I have a page with no data on it. my problem in comdition. when i submiting form on my page i send data to the controller. But when i click on next page i could not send my data from form. How can i send data from the form by paginator?
Sorry for my english! :) "
I have a problem with pagination. when I click on next, after reloading page I have a page with no data on it. my problem in comdition. when i submiting form on my page i send data to the controller. But when i click on next page i could not send my data from form. How can i send data from the form by paginator?
Sorry for my english! :)
a little code
View Template:
for example!!!! This form..
<form id="find_by_anagraphic" method="post" action="findByAnagraphic">
<?php echo $form->input('from', array('options' => $ages, 'label' => 'Age from'));
echo $form->input('to', array('options' => $ages, 'label' => 'to'));
echo $form->input('sex', array('options' => $sex));
echo $form->input('region', array('options' => $regions));
echo $form->input('country_id', array('label'=>'Country','options'=>countriesbyregions['All']));
?>
<?php //$paginator->options(array('data'=>array('from' => 25,'to' => 35))); ?>
<?php echo $form->submit('Submit' , array('name'=>'Task', 'value'=>'Submit')); ?>
<?php echo $form->end(); ?>
</form>
.....Some code goes here (show tables an other)!!!
.. paging with paginator
<?php echo $paginator->prev('<< '.__('previous', true), array(HOW CAN I PUT HERE MY FORM DATA?),null,array('class'=>'disabled'));?>
| <?php echo $paginator->numbers();?>
<?php echo $paginator->next(__('next', true).' >>', array(HOW CAN I PUT HERE MY FORM DATA?), null, array('class'=>'disabled'));?>
Keep up the good work!
$filters = array("lower(Customer.name) like '%".low($search)."%'");for the LIKE statement of MySql automatically compares lowercase.
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.
Just a small question:
Controller Class:
<?phpfunction 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?
The second parameter needs to be a conditions array, not a string.
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?
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') ) );
This doesn't work for me. Is there any known issue for this?
Any help would be appreciated. Thanks a ton!!!
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:
to:paginate($object = null, $scope = array(), $whitelist = array())
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!}
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 ???
Have the same problem..
Is there any solution for this problem ?
$this->SubSubCategory->bindModel(array(
'belongsTo' => array(
'Category' => array(
'className' => 'Category',
'foreignKey' => false,
'conditions' => 'Category.id=SubCategory.category_id',
'fields' => array()
)
)
), false
);
It's simple :)
Searching view is broken? Looks it is missing at least a submit button or I missing something?
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.
in the function sortKey toreturn preg_replace('/.*\./', '', key($options['order']));
Worked fine for me.return preg_replace( sprintf( '/%s\./', $this->defaultModel()), '', key($options['order']));
Cheers
Thanks all!
Mootools: http://mootools.net
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.
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.