One of the main obstacles of creating flexible and user-friendly web applications is designing an intuitive user interface. Many applications tend to grow in size and complexity quickly, and designers and programmers alike find they are unable to cope with displaying hundreds or thousands of records. Refactoring takes time, and performance and user satisfaction can suffer.
Displaying a reasonable number of records per page has always been a critical part of every application and used to cause many headaches for developers. CakePHP eases the burden on the developer by providing a quick, easy way to paginate data.
Pagination in CakePHP is offered by a component in the controller, to make
building paginated queries easier. In the View PaginatorHelper
is
used to make the generation of pagination links & buttons simple.
In the controller, we start by defining the query conditions pagination will use
by default in the $paginate
controller variable. These conditions, serve as
the basis of your pagination queries. They are augmented by the sort
, direction
,
limit
, and page
parameters passed in from the URL. It is important to note
here that the order
key must be defined in an array structure like below:
class PostsController extends AppController {
public $components = array('Paginator');
public $paginate = array(
'limit' => 25,
'order' => array(
'Post.title' => 'asc'
)
);
}
You can also include other find()
options, such as
fields
:
class PostsController extends AppController {
public $components = array('Paginator');
public $paginate = array(
'fields' => array('Post.id', 'Post.created'),
'limit' => 25,
'order' => array(
'Post.title' => 'asc'
)
);
}
Other keys that can be included in the $paginate
array are
similar to the parameters of the Model->find('all')
method, that
is: conditions
, fields
, order
, limit
, page
, contain
,
joins
, and recursive
. In addition to the aforementioned keys, any
additional keys will also be passed directly to the model find methods. This
makes it very simple to use behaviors like ContainableBehavior
with
pagination:
class RecipesController extends AppController {
public $components = array('Paginator');
public $paginate = array(
'limit' => 25,
'contain' => array('Article')
);
}
In addition to defining general pagination values, you can define more than one set of pagination defaults in the controller, you just name the keys of the array after the model you wish to configure:
class PostsController extends AppController {
public $paginate = array(
'Post' => array (...),
'Author' => array (...)
);
}
The values of the Post
and Author
keys could contain all the properties
that a model/key less $paginate
array could.
Once the $paginate
variable has been defined, we can use the
PaginatorComponent
’s paginate()
method from our controller
action. This will return find()
results from the model. It also sets some
additional paging parameters, which are added to the request object. The
additional information is set to $this->request->params['paging']
, and is
used by PaginatorHelper
for creating links.
PaginatorComponent::paginate()
also adds
PaginatorHelper
to the list of helpers in your controller, if it
has not been added already:
public function list_recipes() {
$this->Paginator->settings = $this->paginate;
// similar to findAll(), but fetches paged results
$data = $this->Paginator->paginate('Recipe');
$this->set('data', $data);
}
You can filter the records by passing conditions as second
parameter to the paginate()
function:
$data = $this->Paginator->paginate(
'Recipe',
array('Recipe.title LIKE' => 'a%')
);
Or you can also set conditions
and other pagination settings array inside
your action:
public function list_recipes() {
$this->Paginator->settings = array(
'conditions' => array('Recipe.title LIKE' => 'a%'),
'limit' => 10
);
$data = $this->Paginator->paginate('Recipe');
$this->set(compact('data'));
}
If you’re not able to use the standard find options to create the query you need
to display your data, there are a few options. You can use a
custom find type. You can also implement the
paginate()
and paginateCount()
methods on your model, or include them in
a behavior attached to your model. Behaviors implementing paginate
and/or
paginateCount
should implement the method signatures defined below with the
normal additional first parameter of $model
:
// paginate and paginateCount implemented on a behavior.
public function paginate(Model $model, $conditions, $fields, $order, $limit,
$page = 1, $recursive = null, $extra = array()) {
// method content
}
public function paginateCount(Model $model, $conditions = null, $recursive = 0,
$extra = array()) {
// method body
}
It’s seldom you’ll need to implement paginate() and paginateCount(). You should
make sure you can’t achieve your goal with the core model methods, or a custom
finder. To paginate with a custom find type, you should set the 0
’th
element, or the findType
key as of 2.3:
public $paginate = array(
'popular'
);
Since the 0th index is difficult to manage, in 2.3 the findType
option was
added:
public $paginate = array(
'findType' => 'popular'
);
The paginate()
method should implement the following method signature. To
use your own method/logic override it in the model you wish to get the data
from:
/**
* Overridden paginate method - group by week, away_team_id and home_team_id
*/
public function paginate($conditions, $fields, $order, $limit, $page = 1,
$recursive = null, $extra = array()) {
$recursive = -1;
$group = $fields = array('week', 'away_team_id', 'home_team_id');
return $this->find(
'all',
compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive', 'group')
);
}
You also need to override the core paginateCount()
, this method
expects the same arguments as Model::find('count')
. The example
below uses some PostgresSQL-specifc features, so please adjust
accordingly depending on what database you are using:
/**
* Overridden paginateCount method
*/
public function paginateCount($conditions = null, $recursive = 0,
$extra = array()) {
$sql = "SELECT
DISTINCT ON(
week, home_team_id, away_team_id
)
week, home_team_id, away_team_id
FROM
games";
$this->recursive = $recursive;
$results = $this->query($sql);
return count($results);
}
The observant reader will have noticed that the paginate method
we’ve defined wasn’t actually necessary - All you have to do is add
the keyword in controller’s $paginate
class variable:
/**
* Add GROUP BY clause
*/
public $paginate = array(
'MyModel' => array(
'limit' => 20,
'order' => array('week' => 'desc'),
'group' => array('week', 'home_team_id', 'away_team_id')
)
);
/**
* Or on-the-fly from within the action
*/
public function index() {
$this->Paginator->settings = array(
'MyModel' => array(
'limit' => 20,
'order' => array('week' => 'desc'),
'group' => array('week', 'home_team_id', 'away_team_id')
)
);
}
In CakePHP 2.0, you no longer need to implement paginateCount()
when using
group clauses. The core find('count')
will correctly count the total number
of rows.
By default sorting can be done with any column on a model. This is sometimes
undesirable as it can allow users to sort on un-indexed columns, or virtual
fields that can be expensive to calculate. You can use the 3rd parameter of
PaginatorComponent::paginate()
to restrict the columns that sorting will be
done on:
$this->Paginator->paginate('Post', array(), array('title', 'slug'));
This would allow sorting on the title and slug columns only. A user that sets sort to any other value will be ignored.
The number of results that are fetched per page is exposed to the user as the
limit
parameter. It is generally undesirable to allow users to fetch all
rows in a paginated set. The maxLimit
option asserts that no one can set
this limit too high from the outside. By default CakePHP limits the maximum
number of rows that can be fetched to 100. If this default is not appropriate
for your application, you can adjust it as part of the pagination options, for
example reducing it to 10
:
public $paginate = array(
// other keys here.
'maxLimit' => 10
);
If the request’s limit param is greater than this value, it will be reduced to
the maxLimit
value.
In previous versions of CakePHP you could only generate pagination links using
named parameters. But if pages were requested with GET parameters they would
still work. For 2.0, we decided to make how you generate pagination parameters
more controlled and consistent. You can choose to use either querystring or
named parameters in the component. Incoming requests will accept only the chosen
type, and the PaginatorHelper
will generate links with the chosen type of
parameter:
public $paginate = array(
'paramType' => 'querystring'
);
The above would enable querystring parameter parsing and generation. You can
also modify the $settings
property on the PaginatorComponent:
$this->Paginator->settings['paramType'] = 'querystring';
By default all of the typical paging parameters will be converted into GET arguments.
Note
You can run into a situation where assigning a value to a nonexistent property will throw errors:
$this->paginate['limit'] = 10;
will throw the error “Notice: Indirect modification of overloaded property $paginate has no effect.” Assigning an initial value to the property solves the issue:
$this->paginate = array();
$this->paginate['limit'] = 10;
//or
$this->paginate = array('limit' => 10);
Or just declare the property in the controller class:
class PostsController {
public $paginate = array();
}
Or use $this->Paginator->settings = array('limit' => 10);
Make sure you have added the Paginator component to your $components array if
you want to modify the $settings
property of the PaginatorComponent.
Either of these approaches will solve the notice errors.
As of 2.3 the PaginatorComponent will throw a NotFoundException when trying to access a non-existent page, i.e. page number requested is greater than total page count.
So you could either let the normal error page be rendered or use a try catch block and take appropriate action when a NotFoundException is caught:
public function index() {
try {
$this->Paginator->paginate();
} catch (NotFoundException $e) {
//Do something here like redirecting to first or last page.
//$this->request->params['paging'] will give you required info.
}
}
It’s very easy to incorporate AJAX functionality into pagination.
Using the JsHelper
and RequestHandlerComponent
you can
easily add AJAX pagination to your application. See AJAX Pagination for
more information.
Check the PaginatorHelper
documentation for how to create links for
pagination navigation.