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 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 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 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)

<h2>Search Profiles</h2>
<?php echo $form->create('Profile', array('action' => 'search')); ?>
	<fieldset class="form">
		<?php echo $form->label('Profile.query', 'Query'); ?>
		<?php echo $form->text('Profile.query'); ?>
		<br />
		<?php echo $form->label('Profile.min_rate', 'Minimal rate'); ?>
		<?php echo $form->text('Profile.min_rate'); ?>
		<?php echo $form->error('Profile.min_rate', 'Please use a number.'); ?>
		<br />
		<?php echo $form->label('Profile.max_rate', 'Maximum rate'); ?>
		<?php echo $form->text('Profile.max_rate'); ?>
		<?php echo $form->error('Profile.max_rate', 'Please use a number.'); ?>
		<br />
		<?php echo $form->submit('Szukaj', array('class' => 'submit')); ?>
	</fieldset>
</form>

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 ;)):

<?php $paginator->options(array('url' => $this->passedArgs)); ?>
<h2>Search results</h2>
<p><?php 
$link = array_merge($this->passedArgs, array('action' => 'search'));
echo $html->link('<< Search form', $link, array('title' => 'Get back to the search form')); ?></p>
<?php if(count($profiles)): ?>
	<table cellpadding="0" cellspacing="0">
		<tr>
			<th><?php echo $paginator->sort('Username', 'username'); ?></th>
			<th><?php echo $paginator->sort('Rating', 'rating');?></th>
			<th><?php echo $paginator->sort('Rate', 'rate');?></th>
		</tr>
	<?php 
	$i = 0;
	foreach ($profiles as $profile): 
		$class = null;
		if ($i++ % 2 == 0) {
			$class = 'altrow';
		}
		?>
		<tr<?php echo empty($class) ? '' : ' class="'.$class.'"';?>>
			<td><?php echo $html->link($profile['User']['username'], array('controller' => 'users', 'action' => 'view', $profile['User']['id'])); ?></td>
			<td><?php echo $html->output($profile['User']['rating']); ?></td>
			<td><?php echo $html->output($profile['Profile']['rate']); ?></td>
			<td>
		</tr>
	<?php endforeach; ?>
	</table>
 
	<div class="paging">
	<p>Page <?php echo $paginator->counter(array('separator' => ' of ')); ?> </p>
	<?php echo $paginator->prev('<< ', array(), null, array('class'=>'disabled'));?>
	 | <?php echo $paginator->numbers();?>
	<?php echo $paginator->next(' >>', array(), null, array('class'=>'disabled'));?>
	</div>
<?php else: ?>
	<p>No search results.</p>
	<p>You can <?php echo $html->link('go back to the form', $link, array('title' => 'Go back to the search form')); ?> and change the search criteria.</p>
<?php endif; ?>

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. — Marcin Domanski 2007/07/24 10:04

~~DISCUSSION:off~~

 
paginating_search_results.txt · Last modified: 2008/05/19 17:25 by kabturek
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki