Search Forms in CakePHP

In this CakePHP tutorial I would like to show you how I handle search forms, while preserving pagination.

The basic principal is to read the posted variables, and redirect the user to a page with the appropriate filters in the URL.

Add the Search Form

Lets add a search form on the index page to search through the records. index.ctp
<?php echo $form->create('Post',array('action'=>'search'));?>
	<fieldset>
 		<legend><?php __('Post Search');?></legend>
	<?php
		echo $form->input('Search.keywords');
		echo $form->input('Search.id');
		echo $form->input('Search.name',array('after'=>__('wildcard is *',true)));
		echo $form->input('Search.body',array('after'=>__('wildcard is *',true)));
		echo $form->input('Search.active',array(
			'empty'=>__('Any',true),
			'options'=>array(
				0=>__('Inactive',true),
				1=>__('Active',true),
			),
		));
		echo $form->input('Search.created', array('after'=>'eg: >= 2 weeks ago'));
		echo $form->input('Search.category_id');
		echo $form->input('Search.tag');
		echo $form->input('Search.tag_id');
		echo $form->submit('Search');
	?>
	</fieldset>
<?php echo $form->end();?>

Controller Method

The search action will handle reading the posted variables, and redirecting back to the index url. The index action will handle setting up the pagination options. controllers/posts_controller.php
<?php
class PostsController extends AppController {
	var $name = 'Posts';

	function search() {
		// the page we will redirect to
		$url['action'] = 'index';
		
		// build a URL will all the search elements in it
		// the resulting URL will be 
		// example.com/cake/posts/index/Search.keywords:mykeyword/Search.tag_id:3
		foreach ($this->data as $k=>$v){ 
			foreach ($v as $kk=>$vv){ 
				$url[$k.'.'.$kk]=$vv; 
			} 
		}

		// redirect the user to the url
		$this->redirect($url, null, true);
	}
	function index() {
		// the elements from the url we set above are read  
		// automagically by cake into $this->passedArgs[]
		// eg:
		// $passedArgs['Search.keywords'] = mykeyword
		// $passedArgs['Search.tag_id'] = 3

		// required if you are using Containable
		// requires Post to have the Containable behaviour
		//$contain = array();  

		// we want to set a title containing all of the 
		// search criteria used (not required)		
		$title = array();
		
		//
		// filter by id
		//
		if(isset($this->passedArgs['id'])) {

			// set the conditions
			$this->paginate['conditions'][]['Post.id'] = $this->passedArgs['id'];

			// set the Search data, so the form remembers the option
			$this->data['Search']['id'] = $this->passedArgs['id'];

			// set the Page Title (not required)
			$title[] = __('ID',true).': '.$this->passedArgs['id'];
		}

		//
		// filter by keywords
		//
		if(isset($this->passedArgs['Search.keywords'])) {
			$keywords = $this->passedArgs['Search.keywords'];
			$this->paginate['conditions'][] = array(
				'OR' => array(
					'Post.name LIKE' => "%$keywords%",
					'Post.body LIKE' => "%$keywords%",
				)
			);
			$this->data['Search']['keywords'] = $keywords;
			$title[] = __('Keywords',true).': '.$keywords;
		}

		//
		// filter by name
		//
		if(isset($this->passedArgs['Search.name'])) {
			$this->paginate['conditions'][]['Post.name LIKE'] = str_replace('*','%',$this->passedArgs['Search.name']);
			$this->data['Search']['name'] = $this->passedArgs['Search.name'];
			$title[] = __('Name',true).': '.$this->passedArgs['Search.name'];
		}

		//
		// filter by body
		//
		if(isset($this->passedArgs['Search.body'])) {
			$this->paginate['conditions'][]['Post.body LIKE'] = str_replace('*','%',$this->passedArgs['Search.body']);
			$this->data['Search']['body'] = $this->passedArgs['Search.body'];
			$title[] = __('Body',true).': '.$this->passedArgs['Search.body'];
		}

		//
		// filter by active
		//
		if(isset($this->passedArgs['Search.active'])) {
			$this->paginate['conditions'][]['Post.active'] = ($this->passedArgs['Search.active'])?1:0; 
			$this->data['Search']['active'] = $this->passedArgs['Search.active'];
			$title[] = ($this->passedArgs['Search.active']) ? __('Active Posts',true) : __('Inactive Posts',true);
		}
		
		//
		// filter by created
		// allowing searches starting with <, >, <=, >=
		// allow human dates "2 weeks ago", "last thursday"
		//
		if(isset($this->passedArgs['Search.created'])) {
			$field = '';
			$date = explode(' ',$this->passedArgs['Search.created']);
			if (isset($date[1]) && in_array($date[0],array('<','>','<=','>='))) { 
				$field = ' '.array_shift($date);
			}
			$date = implode(' ',$date);
			$date = date('Y-m-d',strtotime($date));  
			$this->paginate['conditions'][]['Post.created'.$field] = $date;
			$this->data['Search']['created'] = $this->passedArgs['Search.created'];
			$title[] = 'Created: '.$this->passedArgs['Search.created'];
		}

		//
		// filter by category_id, including all children
		//
		if (isset($this->passedArgs['Search.category_id'])) {

			// get all children
			$category_ids = array($this->passedArgs['Search.category_id']);
			$children = $this->Post->Category->children($this->passedArgs['Search.category_id']);
			foreach ($children as $child) {
				$category_ids[] = $child['Category']['id'];
			}

			// set the conditions - SELECT ... WHERE field IN(1,2,3)
			$this->paginate['conditions'][]['Post.category_id'] = $category_ids;

			// set the Search data, so the form remembers the option
			$this->data['Search']['category_id'] = $this->passedArgs['Search.category_id'];

			// set the Page Title to the Category Name
			$title[] = __('Category',true).': '.$this->Post->Category->field('name',array('Category.id'=>$this->passedArgs['Search.category_id']));
		}
		
		//
		// filter by tag (habtm) - name or id
		//
		if (isset($this->passedArgs['Search.tag']) || isset($this->passedArgs['Search.tag_id'])) {

			// if we were given the tag name, get the id
			if (isset($this->passedArgs['Search.tag'])) {
				$this->passedArgs['Search.tag_id'] = $this->Post->Tag->field('id',array('Tag.name'=>$this->passedArgs['Search.tag']));
			}

			// bind the model as a hasOne so we can use the rows as conditions
			$this->Post->bindModel(array(
				'hasOne' => array(
					'PostTag' => array(
						'className' => 'PostTag',
						'foreignKey' => 'post_id',
						'conditions' => array(
							'PostTag.tag_id' => $this->passedArgs['Search.tag_id'],
						),
					),
				),
			),false);
			//$contain[] = 'PostTag'; // required if you are using Containable 
			
			// set the conditions
			$this->paginate['conditions'][]['PostTag.tag_id'] = $this->passedArgs['Search.tag_id'];

			// set the Search data, so the form remembers the option
			$this->data['Search']['tag_id'] = $this->passedArgs['Search.tag_id'];

			// set the Page Title to the Tag Name
			$title[] = __('Tag',true).': '.$this->Post->Tag->field('name',array('Tag.id'=>$this->passedArgs['Search.tag_id']));
		}

		// get posts		
		//$this->Post->contain($contain); // required if you are using Containable
		//$this->paginate['reset']=false; // required if you are using Containable
		$posts = $this->paginate();
		
		// set the category path of each post (not required, just an example)
		// requires Category to have the Tree behaviour
		//
		// you can use this to add anything you want to the $posts array
		// before it is sent to the view
		foreach($posts as $k=>$post) {
			$posts[$k]['CategoryPath'] = $this->Post->Category->getPath($post['Post']['category_id']);
			$posts[$k]['CategoryPath'] = $posts[$k]['CategoryPath']?$posts[$k]['CategoryPath']:array(); 
		}

		// set title
		$title = implode(' | ',$title);
		$title = (isset($title)&&$title)?$title:__('All Posts',true);
		
		// set related data
		$tags = $this->Post->Tag->find('list');
		$this->set(compact('posts','tags','title'));
	}

}

Getting Pagination to Remember Search Options

A quick trick to get pagination to remember the search options.

views/posts/index.php
<?php $paginator->options(array('url' => $this->passedArgs)); ?>

Hiding the Search Form

Here is some JavaScript that will allow you to hide the search form when it's not needed.

This Javascript requires jQuery.

views/posts/index.php
<?php echo $html->link(__('Search', true), 'javascript:void(0)', array('class'=>'search-toggle')); ?>
$(document).ready(function(){
	// toggle the search form
	$('.search-toggle').click(function(){
		$('#PostSearchForm').toggle();
	});
	$('#PostSearchForm').hide();
});

The Final Product

Here is an example of how the form will look.

[inline:search-forms-in-cakephp.jpg]
File attachments: 

Comments

Add new comment

helo. you can share demo for cakephp serch ?

Guest's picture

Search.keywords? Does it mean we need a Search model?

Guest's picture

no, its just to name the field like name="data[Search][keywords]"

brett's picture

Awesome tutorial
Learnt a lot in this tutorial

I wanted to redirect using POST rather than GET
But got to know that it cannot be done .

If you know any method please suggest

Thank you

Guest's picture

Hi all,
I am newbie in cakephp. I have a problem with my code when i try doing by these steps, maybe because i do not understand clearly. Could you tell me about database of this tutorial ? Post(id, name , body, active)? and what about " var $name = 'Posts';" in posts_controller file?
Could you send me your project +database, i really want to reference. Thanks !

Guest's picture

Great post!!!

I am trying to implement this on something other than Posts and only on the end_time. When I input <1 month ago, I don't get anything. When I look and the db or another listing, I have plenty that fit the bill.

If I use < 1 month ago the pagination shows the right number, but there are more in the table.

Guest's picture

Thanks for this good job.

I get the code but when I click on Search the result page don't show me any records. I have not any error message, only the empty table. I note that the url have the format ....index/Search.name:...

Can you help me?

Thanks again.

Guest's picture

Great!! But how about hasMany relation? For example Hotel hasMany Room. i would like to search data from 'room' inside 'hotel' controller. i using search keyword but got

Warning (512): SQL Error: 1054: Unknown column 'Room.name' in 'where clause' [CORE\cake\libs\model\datasources\dbo_source.php, line 684]

Guest's picture

thanks for nice article it's working..

Guest's picture

Hey Bret thank you very much for this post.

I´m wondering if you can update it when the form has a checkboxes and you want to filter by selecting multiple options. Thank you again

Guest's picture

There is a little mistake in your code:

PostTag.tag_id' => $this->passedArgs['Search.tag_id'];
),
),

It's

PostTag.tag_id' => $this->passedArgs['Search.tag_id']
),
),

;) Otherwise you would get a "syntax error, unexpected ';', expecting ')'"

Guest's picture

thanks, I have updated the example code

brett's picture

Nice one! But can you publish your tables for comparison? I got some sql-errors with mine, after it calls the search() function. (I'm only using the parts for filter by tag and tag_id)

Guest's picture

Hey, I was wondering why I cannot search for keywords with special characters such as '#' pound...

Everything works, but in the index action, the passedArgs['Search.keywords'] is empty is i search for #

can you help me out please ?

Thanks!

Guest's picture

Many thanks for this. Worked like a charm.

Guest's picture

Hi I ve tried to implemented your search, but it error form me like

Warning (2): Invalid argument supplied for foreach() [APP\controllers\artists_controller.php, line 21]

thank

Guest's picture

Thanks mate, it's easy n guuuuuuuuuud!

Guest's picture

What sort of modifications would be required if the paginate conditions were a habtm relationship?

Guest's picture

none, just have a look at the Tag section, where it says "filter by tag (habtm) - name or id"

brett's picture

Hello. Thanks for nice post.

I have one question. What happened when a date field is submitted? I found date select field will not working. here is my example debug info for $this->data


Array
(
[search] => Array
(
[id] =>
[name] =>
[username] =>
[expdate] => Array
(
[month] => 06
[day] => 01
[year] => 2010
)

[show] => 1
)

)

I think url builder loop needs to be modified. How can I do that?? Thanks

Guest's picture

Great Post !

It really helped me to build my fist Cakephp Application.

Thanks

Guest's picture

OMG,Why I cant see the data after I input keyword?
but the link is already changed. ><

Guest's picture

Great article. It really should be in The Bakery, as it actually works! :)

I know I'm nit-picking, but shouldn't ,
$title = (isset($title)&&$title)?$title:'All Videos';
be
$title = (isset($title)&&$title)?$title:__('All Videos', true);

If you haven't submitted it to the bakery, you really should. Thanks again.

Rob

Guest's picture

Hi Robert, thanks for pointing that out. I updated it to __('All Posts', true).

brett's picture

Thanks dooooddee!!!
It's rockin'!!!

Guest's picture

I have some problems when I used your code,
and I believed the problem is how I create my database.

The error messages:

Notice (8): Undefined property: Post::$Tag [APP/controllers/posts_controller.php, line 166]

Code | Context

$title = "All Videos"
$posts = array()

// set related data
$tags = $this->Post->Tag->find('list');

PostsController::index() - APP/controllers/posts_controller.php, line 166
Object::dispatchMethod() - CORE/cake/libs/object.php, line 116
Dispatcher::_invoke() - CORE/cake/dispatcher.php, line 227
Dispatcher::dispatch() - CORE/cake/dispatcher.php, line 194
[main] - APP/webroot/index.php, line 88

Fatal error: Call to a member function find() on a non-object in /var/www/cake/app/controllers/posts_controller.php on line 166

This is my table:

+-------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| keywords | varchar(20) | YES | | NULL | |
| id | int(11) | NO | PRI | NULL | |
| name | varchar(30) | YES | | NULL | |
| body | text | YES | | NULL | |
| category_id | int(11) | YES | | NULL | |
| tag_id | int(11) | YES | | NULL | |
| list | varchar(30) | YES | | NULL | |
| tag | varchar(30) | YES | | NULL | |
+-------------+-------------+------+-----+---------+-------+
Can you please show me how you create table in mysql?

Thank you so much

Guest's picture

Hi, I try use your code for my app, but I use i18n and when I want search by field 'name', but i drop this column from 'products' table, because data is in 'i18n' table and I don't know How I can repaire? Maybe some solutions?

Guest's picture

instead of using Post.name, use PostI18n.name (or whatever your i18n table name is).

EG:

FIND
$this->paginate['conditions'][]['Post.name LIKE'] = str_replace('*','%',$this->passedArgs['Search.name']);

REPLACE WITH
$this->paginate['conditions'][]['PostI18n.name LIKE'] = str_replace('*','%',$this->passedArgs['Search.name']);

brett's picture

Hi, could you explain how the selects in the search form get populated?

Guest's picture

the SQL arguments are set from $this->paginate

EG:
$this->paginate['conditions'][]['Post.id'] = $this->passedArgs['id'];

brett's picture

Thanks a lot for this nice article. I tried your code, it works, except that the ID search is not working to me..
Do not know why!?

Guest's picture

awesome. thx!

Guest's picture

I noticed a small error in the URL builder loop in the Post controller's search action. The URL that I was getting for a form with just a keyword field was /post/index/Search.keywords:Array. I believe the correct code should be:

<?php
foreach ($this->data as $k=>$v){
foreach ($v as $kk=>$vv){
$url[$k.'.'.$kk]=$vv;
}
}
?>

Thanks for the article. I'm new to CakePHP and still trying to figure things out. This helps some in understanding how forms work.

Spot on with this write-up, I absolutely believe this web site needs much more
attention. I'll probably be back again to read through more, thanks for the info!

nice spotting, I have now updated the page.

thanks! =)

brett's picture

Looks like you forgot to update the page :)

Guest's picture

ok, take 2...

brett's picture

Feeling social? Share this page!