====== Paginating Search Results in Cakephp 1.2 using the PRG pattern====== This tutorial will help you with creating a basic search forms and paginating the results from the search. In this tutorial we will search Profiles by selected criteria. Here is an article about [[http://www.theserverside.com/tt/articles/article.tss?l=RedirectAfterPost|PRG (Post-Redirect-Get)]] if you're not familiar with it. ===== Changelog ===== Important change in named parameters handling. 2007/09/04 02:47 From revision 5535 naming parameters handling changed - it's now required to specify the allowed parameters. You can do that by using Router::connectNamed(). the first param is the array of allowed named params, the second param is an array of options (the only option used here is $options['argSeparator'] which specifies the seperator - ':' by default). Router::connectNamed(array('query','cat_id', 'sub_cat_id', 'budget', 'length')); Probably the best place for it is in config/routes.php but you can put it anywhere in the code(like in the search() action in controller). 2007/08/03 17:44 I've used $this->params['pass'] all across the tutorial - it contained the passed named params until revision 5460. It appears it was not the right way to do it :) One should use $this->passedArgs. I've changed it in the tutorial - a simple search&replace is enough. Thx for gwoo for [[https://trac.cakephp.org/ticket/3002|clarification]]. ===== Database ===== Let's start with a simple database: CREATE TABLE profiles ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, user_id INTEGER UNSIGNED NOT NULL, skills TEXT NULL, other TEXT NULL, rate INTEGER UNSIGNED NULL, resume TEXT NULL, modified DATETIME NULL, created DATETIME NULL, PRIMARY KEY(id), FULLTEXT INDEX profiles_search(skills, other) ); ===== ProfileController::search ===== We're going to need 2 actions in the ProfilesController: **search** - that will render the form and **results** that will show the results to the user and do the actual searching. /** * search * * We will use the Post Redirect Get pattern here * user will be redirected to the result function * **/ function search() { //set the data to the passed params but only if there wasnt a POST (used when GETing to the form) if(empty($this->data) && !empty($this->passedArgs)) { $data['Profile'] = $this->passedArgs; } //if there is data POSTed if (!empty($this->data)) { // set the data $this->Profile->data = $this->data; // validate if ($this->Profile->valid()) { $params = $this->data['Profile']; $params['action'] = 'results'; // refirect with the POSTed params as GET $this->redirect($params); } else { //there were errors $this->Session->setFlash('Please correct the errors below.'); } } // populate the form with the data if(!empty($data)) { $this->data = $data; } } This action is pretty straight it validates the data before sending it to the **results** action and show the errors in the form. The first condition is used when the user is coming back from results to the search forms (for example when there were no results) so the form is populated with the criteria. If the data validates then the criteria array is passed to ProfilesController::results using [[http://bakery.cakephp.org/articles/view/passing-named-parameters|Named Parameters]] introduced in CakePHP 1.2 . Here is the view for ProfileController::search() (views/profiles/search.ctp) (Yes I'm used to using the individual FormHelper methods - you could replace them with FormHelper::input() here)

Search Profiles

create('Profile', array('action' => 'search')); ?>
label('Profile.query', 'Query'); ?> text('Profile.query'); ?>
label('Profile.min_rate', 'Minimal rate'); ?> text('Profile.min_rate'); ?> error('Profile.min_rate', 'Please use a number.'); ?>
label('Profile.max_rate', 'Maximum rate'); ?> text('Profile.max_rate'); ?> error('Profile.max_rate', 'Please use a number.'); ?>
submit('Szukaj', array('class' => 'submit')); ?>
===== ProfileController::results ===== /** * results * * shows the actual results from the search * **/ function results() { // if someone goes directly to result without any params if(empty($this->passedArgs)) { $this->Session->setFlash('Please choose the search criteria.'); $this->redirect(array('action' => 'search'), false, true); } // set the data for valdiation $this->Profile->data['Profile'] = $this->passedArgs; // valid with custom function if($this->Profile->validate()) { // parse the conditions (whitelist) $parsedConditions = $this->Profile->parseCriteria($this->passedArgs); // we want the user data returned with the profile data $this->Profile->recursive = 1; $this->Profile->expects(array('Profile','User')); // we can add conditions that will be always applied $parsedConditions['Profile.is_active'] = 1; $this->paginate['Profile'] = array( 'conditions' => $parsedConditions, 'fields' => 'User.id, User.username, User.rating, Profile.id, Profile.rate' ); // paginate the results $this->set('profiles', $this->paginate()); } else { //if the params are not valid -> return to the search form and populate the form $redirect = $this->passedArgs; $redirect['action'] = 'search'; $this->redirect($redirect, false, true); } } The second action GETs the search criteria from **ProfileController::search**. It passes the criteria to a model function **Profile::parseCriteria** which returns an array of conditions that will be applied to the model. Here is the view (simplified ;)): options(array('url' => $this->passedArgs)); ?>

Search results

passedArgs, array('action' => 'search')); echo $html->link('<< Search form', $link, array('title' => 'Get back to the search form')); ?>

>
sort('Username', 'username'); ?> sort('Rating', 'rating');?> sort('Rate', 'rate');?>
link($profile['User']['username'], array('controller' => 'users', 'action' => 'view', $profile['User']['id'])); ?> output($profile['User']['rating']); ?> output($profile['Profile']['rate']); ?>

Page counter(array('separator' => ' of ')); ?>

prev('<< ', array(), null, array('class'=>'disabled'));?> | numbers();?> next(' >>', array(), null, array('class'=>'disabled'));?>

No search results.

You can link('go back to the form', $link, array('title' => 'Go back to the search form')); ?> and change the search criteria.

I used $paginator->options so that all url's created by paginator will have our search criteria in them. With the array_merge I'm sure that the link will point to the search form (could be overwritten by changing the url). ===== Profile::parseCriteria ===== /** * parseCriteria * * parses the GET data and returns the conditions for the findAll/paginate * we are just going to test if the params are legit * * @param array $data criteria **/ function parseCriteria($data) { $conditions = array(); if(!empty($data['min_rate'])) { $conditions[] = 'Profile.rate >'.$data['min_rate']; } if(!empty($data['max_rate'])) { $conditions[] = 'Profile.rate <'.$data['max_rate']; } if(!empty($data['query'])) { $conditions[] = 'MATCH ( skills, other) AGAINST (\''.$data['query'].'*\' IN BOOLEAN MODE) '; } return $conditions; } This function parses the criteria and creates the conditions. I use a custom function so i can construct different conditions and i am sure that only my conditions are passed to the db (white listing). ===== Finish ===== And that's it :). Hope you enjoyed the show. Comments are welcome. --- //[[blog@kabturek.info|Marcin Domanski]] 2007/07/24 10:04// {{tag>[cakephp pagination prg tutorial]}} ~~DISCUSSION:off~~