CakePHP - search forms and paginating results the easy way, with a new component and helper to boot
I've made many search forms that query thousands of records and can return as many results, thus requiring pagination. For the first three search forms i've created, it ended up being a re-education every time with many conditionals and general monkeying-around with a side of tom-foolery: passing the search form variables to the pagination helper, checking to see if search parameters have passed, if not then set defaults, etc, etc...
So after the third search form I decided to spend a little extra time to make the process more streamlined, and hope you can find it useful as well.
There are two parts to streamlining this process. The first part is setting up the PaginateFormVariables component in the controller. First, create the paginate_form_variables.php file in your app/controller/components folder and insert this code:
<?php
class PaginateFormVariablesComponent extends Object {
var $nonPaginatorArgs = array();
function startup(&$controller) {
$this->controller =& $controller;
}
function beforeRender(&$controller) {
$this->controller->params['paging']['nonPaginatorArgs'] = $this->nonPaginatorArgs;
}
function shutdown(&$controller) {
}
//returns $nonPaginatorArgs for use with view $paginator
function engage($defaultFormData = array(),$removeNullValues = true){
//if filterNullValues = true, then filter:
if($removeNullValues){
$defaultFormData = $this->filterNullValues($defaultFormData);
if(!empty($this->controller->data)){
$this->controller->data = $this->filterNullValues($this->controller->data);
}
}
///merge defaultFormData with post data (if any), else set to defaults:
$this->controller->data = ($this->controller->data)? array_merge($defaultFormData, $this->controller->data) : $defaultFormData;
//convert form data to passedArgs for paginate
$this->nonPaginatorArgs = $this->convertDataToPassedArgs($this->controller->data);
if(!empty($this->controller->passedArgs)){
//if passedArgs exist, indicates a get request from the paginator.
//convert back to $this->controller->data for form auto population, removing paginator vals
$this->controller->passedArgs = array_merge($this->nonPaginatorArgs,$this->controller->passedArgs);
$this->nonPaginatorArgs = $this->filterPaginatorArgs($this->controller->passedArgs);
$this->controller->data = $this->convertPassedArgsToData($this->nonPaginatorArgs);
}else{
$this->controller->passedArgs = $this->nonPaginatorArgs;
}
$this->controller->paginateformVariables = $this->nonPaginatorArgs;
}
private function convertDataToPassedArgs($data = array()){
$retArray = array();
foreach($data as $bigKey=>$d){
foreach($d as $key=>$val){
$retArray[$bigKey.'.'.$key]=$val;
}
}
return $retArray;
}
//must have keys be in Model.field format
private function convertPassedArgsToData($args = array()){
$args = $this->filterPaginatorArgs($args);
$retArray = array();
foreach($args as $key=>$arg){
if(strpos($key, '.')){
$splitter = split('\.',$key);
$retArray[$splitter[0]][$splitter[1]] = $arg;
}else{
$retArray[$key] = $arg;
}
}
return $retArray;
}
private function filterPaginatorArgs($args = array()){
$retArray = array();
foreach($args as $key=>$arg){
if( !($key == 'page' || $key == 'sort' || $key == 'direction') ){
$retArray[$key] = $arg;
}
}
return $retArray;
}
private function filterNullValues($args = array()){
foreach($args as $bigKey=>$arg){
if(is_array($arg)){
foreach($arg as $key=>$val){
if(empty($val)){
unset($args[$bigKey][$key]);
}
}
}
}
foreach($args as $bigKey=>$arg){
if(empty($arg)){
unset($args[$bigKey]);
}
}
return $args;
}
}
?>
Now in your controller file make sure to add the component at the top of the class:
var $components = array('PaginateFormVariables');
Now that we have our component initialized, we can now use the component to populate the our modified paginator helper, which i call formVariablesPaginator. All of the passing of the variables happens behind the scenes. It specifically creates an array within the paginate array that contains all the search form data, and also poulates $this->data with the posted form data or it's defaults, which are also set before hand. but to get that search form data to the formVariablesPaginator we have to pass some arguments to the engage method like so:
/*set-up Form default values in case the form didn't post. This will auto-populate *the Form values ($this->data) as well as save alot of hassle with excessive *conditionals (for queries, etc) if we didn't set defaults. Now we can code *as the values are always set): */ $defaultFormData = array(); $defaultFormData['Language']['language_code_id'] = ''; $defaultFormData['Inventory']['search'] = 'Inventory'; /*run the PaginateFormVariables->engage method. This component will convert $this->data * to $this->passedArgs and back where appropriate, and will also pass non- * paginator arguments (args from the form): */ $this->PaginateFormVariables->engage($defaultFormData,false);
This specific example is from one of my latest websites at the time of this posting and can be tested here: phoible.org/inventories/ the form contains a text search input and a drop-down input to either search language inventories that are in PHOIBLE.org or all languages. If you test the search form out you can see everything working as expected, pagination and column sorting alike.
Now for a brief breakdown of the above code. The $defaultFormData is what it says it is. simply populate this array with the default form data you wish. Simply use the cake $array['Model']['input'] convention for all of your form inputs. My example has two form inputs. Then, you pass this array as the first argument of the PaginateFormVariables->engage() method. The second argument is a boolean variable, set to true if you want to strip out null or empty values, false if not (true is the default value).
The next step is to finish the action as you would without the component- set pagination conditions (if any), set the paginate var if you need to, and manipulate any search form data. In my example I have the following set to, give you an idea:
//set up pagination defaults and params:
$this->Inventory->Behaviors->attach('Containable');
$this->Language->Behaviors->attach('Containable');
$this->paginate = array(
'Inventory' => array(
'recursive'=>1,
'contain' => array('Language'),
'limit' => 50
),
'Language' => array(
'recursive'=>1,
'contain' => array('Inventory'),
'limit' => 50
)
);
//make your pagination query conditions
$paginationConditions = array();
$paginationConditions['OR']['Language.language_code_id LIKE ']='%'.$this->data['Language']['language_code_id'].'%';
$paginationConditions['OR']['Language.name LIKE ']='%'.$this->data['Language']['language_code_id'].'%';
//check Form search dropdown condition for pagination type: 'Language' or 'Inventory'){
$languages = $this->paginate($this->data['Inventory']['search'],$paginationConditions);
$this->set(compact('languages'));
That takes care of the controller. Now you'll have to add the helper. Create a file in app/view/helpers called form_variables_paginator.php and save the code below into it:
<?php
App::import('Helper', 'Paginator');
class FormVariablesPaginatorHelper extends PaginatorHelper{
function sort($title, $key = null, $options = array()) {
$options = array_merge(array('url' => array(), 'model' => null), $options);
if(isset($this->params['paging']['nonPaginatorArgs'])){
$options['url'] = array_merge($this->params['paging']['nonPaginatorArgs'],$options['url']);
return parent::sort($title,$key,$options);
}else{
///error
//debug('Error: Must have PaginateFormVariables Component set in Controller');
}
}
function prev($title = '<< Previous', $options = array(), $disabledTitle = null, $disabledOptions = array()) {
$options = array_merge(array('url' => array()), $options);
if(isset($this->params['paging']['nonPaginatorArgs'])){
$options['url'] = array_merge($this->params['paging']['nonPaginatorArgs'],$options['url']);
return parent::prev($title, $options, $disabledTitle, $disabledOptions);
}else{
///error
//debug('Error: Must have PaginateFormVariables Component set in Controller');
}
}
function next($title = 'Next >>', $options = array(), $disabledTitle = null, $disabledOptions = array()) {
$options = array_merge(array('url' => array()), $options);
if(isset($this->params['paging']['nonPaginatorArgs'])){
$options['url'] = array_merge($this->params['paging']['nonPaginatorArgs'],$options['url']);
return parent::next($title, $options, $disabledTitle, $disabledOptions);
}else{
///error
//debug('Error: Must have PaginateFormVariables Component set in Controller');
}
}
function numbers($options = array()) {
$options = array_merge(array('url' => array()), $options);
if(isset($this->params['paging']['nonPaginatorArgs'])){
$options['url'] = array_merge($this->params['paging']['nonPaginatorArgs'],$options['url']);
return parent::numbers($options);
}else{
///error
//debug('Error: Must have PaginateFormVariables Component set in Controller');
}
}
}
?>
Now add the helper in the controller (I lied, we're not quite done with the controller yet! :) ):
var $helpers = array('FormVariablesPaginator');
Ok, now we're done with the controller. The last step is to simply replace any '$paginator' code with '$formVariablesPaginator' in your view and it's done. Play with it, use it, love it, or hate it and let me know what you think!

This looks great -- i will try it. can you post the view you used to generate the the search form?