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.
| Attachment | Size |
|---|---|
| search-forms-in-cakephp.jpg | 21.65 KB |

Comments
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:
<?phpforeach ($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.
nice spotting, I have now updated the page.
thanks! =)
Looks like you forgot to update the page :)
ok, take 2...
awesome. thx!
Hi, could you explain how the selects in the search form get populated?
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?
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
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
(
[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?
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!
Post new comment