Nothing like writing a tutorial on your code to realize how you could improve it! Just minutes after writing a tutorial on persisting pagination limits in CakePHP – a system I developed weeks ago, I wondered to myself why I just didn’t make it a component and boost its portability by a billion percent. So, I whipped it up and called it AutoPaginate. Usage is much simpler; no more AppController silliness, just setting up the view files and individual controllers for pagination. I’ll paste in those parts of the tutorial from version 1.0. Speaking of which:
If you find yourself paginating a lot of results, you may want to give your users the ability to change the number of results per page they see. Easy enough, I know, but the user’s preference should also persist through the session no matter what data the application is serving up. And, ideally, we don’t want to POST every time we want to change the pagination limit; I like having it right in the URL for easy reading.
So here’s a component that shows how easy it is to put in user-modifiable, session-persisting pagination limits.
The full component (app/controllers/components/auto_paginate.php):
<?php
/**
* Auto Paginate Component class.
*
* A simple extension for paginating that helps with persisting user-defined pagination limits.
*
* @filesource
* @author Jamie Nay
* @copyright Jamie Nay
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @link http://jamienay.com/code/auto-paginate-component
*/
class AutoPaginateComponent extends Object {
/**
* Other components needed by this component
*
* @access public
* @var array
*/
public $components = array('Session');
/**
* component settings
*
* @access public
* @var array
*/
public $settings = array();
/**
* Default values for settings.
* - options: the results-per-page options to present to the user.
*
* @access private
* @var array
*/
private $__defaults = array(
'options' => array(1, 5, 10, 25, 50, 100),
'defaultLimit' => 25
);
/**
* Configuration method.
*
* @access public
* @param object $model
* @param array $settings
*/
public function initialize(&$controller, $settings = array()) {
$this->settings = array_merge($this->__defaults, $settings);
$this->controller =& $controller;
}
/**
* beforeRender()
*
* Set the variables needed by the controller.
*
* @access public
* @param $controller Controller object
*/
public function beforeRender(&$controller) {
$controller->set('paginationOptions', $this->settings['options']);
$controller->set('paginationLimit', $this->paginationLimit());
}
/**
*
* Set the controller's $paginate variable.
*
* @access public
* @param array $options
*/
public function setPaginate($options = array()) {
$defaults = array(
'limit' => $this->paginationLimit()
);
$this->controller->paginate = array_merge($defaults, $options);
}
/**
* Set the pagination limit based on user input and session variables.
*
* @access public
*/
public function paginationLimit() {
if (isset($this->controller->params['named']['Paginate'])) {
$this->Session->write('Pagination.limit', $this->controller->params['named']['Paginate']);
}
return ($this->Session->check('Pagination.limit') ? $this->Session->read('Pagination.limit') :
$this->settings['defaultLimit']);
}
}
?>
Two config options: ‘options’, which determines the available results-per-page options for the user, and ‘defaultLimit’, which, predictably, determines the default pagination results limit.
To use the component, just plug it into your AppController’s $components array (app/app_controller.php):
var $components = array('AutoPaginate');
Now we need to change the way we configure pagination in individual controllers. This is the easy part – just call AutoPaginate::setPaginate() instead of explicitly defining $this->paginate when setting up your pagination. For example, in our PlayersController::admin_index() function we’re used to doing this:
$this->paginate = array(
'limit' => 20,
'contain' => array('Player, 'Position')
);
$this->set('players', $this->paginate());
But now we’ll just do this (app/controllers/players_controller.php):
function admin_index() {
$this->AugoPaginate->setPaginate(array(
'contain' => array('Team', 'Position')
));
$this->set('players', $this->paginate());
}
Finally, since our pagination will be standardized through the entire site, we’ll set up a couple of common view elements.
The top element will present the choices for the number of results per page, as well as the usual “display X out of X results” line. It looks like this (app/views/elements/pagination/top.ctp):
Results per page:
<?php
$results = array();
foreach ((array)$paginationOptions as $option) {
if ($paginationLimit == $option) {
$results[] = $option;
} else {
$args = $this->passedArgs;
$args['Paginate'] = $option;
$results[] = $html->link($option, $args);
}
}
echo implode(" | ", $results);
?>
<?php
echo $paginator->counter(array(
'format' => __('Page %page% of %pages%, showing %current% records out of %count% total, starting on record %start%, ending on %end%.', true)
));
?>
That’ll produce a string of plain text links – not a form – that the user can use to change pagination limits. The output looks like this:
Results per page: 1 | 5 | 10 | 25 | 50 | 100 Page 1 of 105, showing 10 records out of 1042 total... (etc)
$paginationOptions and $paginationLimit are set in AutoPaginateComponent.
We’ll also make a new element for the bottom of paginated results too (app/views/elements/pagination/bottom.ctp):
<div class="paging">
<?php echo $paginator->prev('<< '.__('previous', true), array('url' => $this->passedArgs), null, array('class'=>'disabled'));?>
| <?php echo $paginator->numbers(array('url' => $this->passedArgs));?>
<?php echo $paginator->next(__('next', true).' >>', array('url' => $this->passedArgs), null, array('class'=>'disabled'));?></div>
Now to insert the elements into a typical view with typical paginated results. I like to insert top.ctp as a table caption, but you can do anything you’d like. If we’re paginating in, say, PlayersController::admin_index, it might look something like this (app/views/players/admin_index.ctp):
<h2><?php __('Players');?></h2>
<table cellpadding="0" cellspacing="0" class="table_list">
<caption>
<?php echo $this->element('pagination/top'); ?>
</caption>
.... (data in table) ...</table>
<?php echo $this->element('pagination/bottom'); ?>
Pretty simple, eh? I prefer using helper functions to set variables anyway, so this kills two birds in one stone by automatically setting the pagination limit and getting rid of the “$this->paginate =” business in controllers.
As always, comments, suggestions, improvements and helpful criticisms welcome!
8 Responses to Auto Paginate Component for CakePHP (Persisting Pagination Limits 2.0)
fly2279
July 16th, 2009 at 4:25 am
Thanks for making this a component. It saved me a bunch of time.
fly2279
July 16th, 2009 at 4:47 am
I created the component file and the elements and I get some errors. First if I use the _setPaginate method it gives me: Call to undefined method UsersController::_setPaginate(). Also for the views it gives me: Undefined variable: htmla for the top.ctp and Call to a member function link() on a non-object for the bottom.ctp. What did I do wrong?
Jamie
July 16th, 2009 at 8:06 am
Oops, sorry: I kinda took the easy way out and just copied and pasted the code from the version first of this. I thought I made the necessary changes but it looks like I missed a spot or two. instead of $this->_setPaginate, just use $this->AutoPaginate->setPaginate without the underscore.
As for the htmla problem, another oops: I’m using an extension of the HTML helper that’s called HtmlaHelper. So just replace all of the $htmla->link calls with $html->link calls.
I’ve updated the tutorial with the fixes. Thanks for pointing them out!
fly2279
July 16th, 2009 at 9:21 am
@Jamie
Oh, I should have figured that out. Must have been too fried to see that. Thanks for the fixes.
fly2279
July 17th, 2009 at 5:51 am
I think in the bottom.ctp file you should leave the array empty that holds the array(‘url’ => $this->passedArgs) on the prev and next links. The way they are both links point to the current page instead of the previous or next page. I changed it to read (like in the original baked files): echo $paginator->next(__(‘next’, true).’ >>’, array(), null, array(‘class’=>’disabled’))and now the links work fine.
Jamie
July 17th, 2009 at 7:19 am
@fly2279
Well, the problem there is that then you lose any custom prefixes and custom named arguments – at least in my testing. That’s why I need to pass $this->passedArgs, since otherwise my named arguments will vanish. The same thing goes for non-admin prefixes, which I’ve been working with recently. But if you can think of a way to preserve those while still keeping the paginator links nice and lean, then I’m all for it.
fly2279
July 17th, 2009 at 9:48 am
@Jamie
Are your prev and next links working correctly using the original code?
jayu
June 24th, 2010 at 12:14 am
hello,
I used your component but it shows me error array_combine() [function.array-combine]: Both parameters should have an equal number of elements my code is given below. I just change the name of the component as pagination.
in events controller
$this->Event->recursive = 0;
$this->Pagination->setPaginate(array(
‘contain’ => array(‘Event’)
));
$this->data = $this->Event->find(“all”);
$this->set($this->data, $this->Paginate());
Thanks