Submitted by brett on Sun, 05/10/2008 - 12:40am
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
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
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 !
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.
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.
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]
thanks for nice article it's working..
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
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 ')'"
thanks, I have updated the example code
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)
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!
Many thanks for this. Worked like a charm.
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
Thanks mate, it's easy n guuuuuuuuuud!
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"
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
Great Post !
It really helped me to build my fist Cakephp Application.
Thanks
OMG,Why I cant see the data after I input keyword?
but the link is already changed. ><
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).
Thanks dooooddee!!!
It's rockin'!!!
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
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']);
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!?
awesome. thx!
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! =)
Looks like you forgot to update the page :)
ok, take 2...
Add new comment