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.

search-forms-in-cakephp.jpg

AttachmentSize
search-forms-in-cakephp.jpg21.65 KB

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

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.

brett's picture

nice spotting, I have now updated the page.

thanks! =)

Looks like you forgot to update the page :)

brett's picture

ok, take 2...

awesome. thx!

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

brett's picture

the SQL arguments are set from $this->paginate

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

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!?

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?

brett'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']);

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

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

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

brett's picture

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

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

Great Post !

It really helped me to build my fist Cakephp Application.

Thanks

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

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

brett's picture

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

Thanks mate, it's easy n guuuuuuuuuud!

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <b> <i> <strong> <cite> <em> <code> <pre> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <css>, <diff>, <drupal5>, <html>, <javascript>, <php>. The supported tag styles are: <foo>, [foo]. PHP source code can also be enclosed in <?php ... ?> or <% ... %>.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.