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.
Download code
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.
Download code
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.
Download code
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
Download code
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.- 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.
- 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); ?>
Comments
Question
1 What about cake 1.1.x
Comment
2 Ordering for multiple tables
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.
Comment
3 Sorting by another model
Comment
4 Sorting by another model
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.
Comment
5 advanced pagination
Question
6 How to make a Pagination Ajax with mootools
Thanks all!
Mootools: http://mootools.net
Comment
7 Sorting By Another Model
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
Question
8 paginating child model
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.
Question
9 Searching incomplete
Searching view is broken? Looks it is missing at least a submit button or I missing something?
Question
10 its not working
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 ???
Comment
11 simple but powerful workaround
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!}