Well, probably you wont believe me, but I wrote whole 3 pages while I realized that it’s so long and probably this is not the right way /and definitely it’s not easy/, so I scratched everything and start over.
It’s been quite long time since I post an article for CakePHP so here it is – shiny brand new post
This post is more a method how to make filtering, rather a rocket science component, because it’s possible to be used even without extra classes /helpers and components/, but I will post a component which will tune the functionality.
So what is all about? Have you ever miss a filtering functionality in your index actions? Well I really miss it
What is the best technique to make filter?
In this example there is a controller Countries which handle the countries in the application. table contain 3 columns, ID, NAME and CODE. Code column is numeric in this example and it’s not following any real naming or country convention.
1. You need a form in your index.ctp, so here is the code /basically it’s a form taken from add.ctp file
/
echo $form->create('Country', array('action'=>'/index'));
?><fieldset><legend><?php __('Filter');?></legend><?
echo $form->input('name');
echo $form->input('code');
?></fieldset><?
echo $form->end('Search');
?>
You can use your imagination how many fields and the position of them in the form, the tricky part is that you need to define the action parameter in the form tag, otherwise this will point to add action which is obviously not very smart
Ok, That’s it … probably you don’t believe me, but it’s true, that’s it. The only thing which you need to do in the controller is this:
OR
and you are done, voila
Ok, this was the good part, let’s continue with the bad part…
there are 2 things which are missing in the ideal world
1. if you have paginator and the filter return more results than you can see in a page then by clicking on the next>> link the filter will be lost and user will be confused from the results. A consequence of this is that the form will loose the data and user need to type it again…
2. if user search by part of a string let’s say try to search for all countries starting with “Bu” the result will be empty table instead of 3 countries starting with “Bu”, because SQL generated will search like:
Ok, explaining the simple solution and the bad consequences here is the ultimate helper for this – the filter component:
/**
* Filter component
* Benefits:
* 1. Keep the filter criteria in the Session
* 2. Give ability to customize the search wrapper of the field types
**
* @author Nik Chankov
* @website http://nik.chankov.net
* @version 1.0.0
*
*/
class FilterComponent extends Object {
/**
* fields which will replace the regular syntax in where i.e. field = 'value'
*/
var $fieldFormatting = array(
"string"=>"LIKE %s%%",
"date"=>"'%s'"
);
/**
* Function which will change controller->data array
*
* @param object $controller the class of the controller which call this component
* @access public
*/
function process($controller){
$this->_prepareFilter($controller);
$ret = array();
if(isset($controller->data)){
//Loop for models
foreach($controller->data as $key=>$value){
if(isset($controller->{$key})){
$columns = $controller->{$key}->getColumnTypes();
foreach($value as $k=>$v){
if($v != ''){
//Check if there are some fieldFormatting set
if(isset($this->fieldFormatting[$columns[$k]])){
$ret[$key.'.'.$k .' LIKE '] = sprintf($this->fieldFormatting[$columns[$k]], $v);
} else {
$ret[$key.'.'.$k .' LIKE '] = $v;
}
}
}
//unsetting the empty forms
if(count($value) == 0){
unset($controller->data[$key]);
}
}
}
}
return $ret;
}
/**
* function which will take care of the storing the filter data and loading after this from the Session
*/
function _prepareFilter(&$controller){
if(isset($controller->data)){
foreach($controller->data as $model=>$fields){
foreach($fields as $key=>$field){
if($field == ''){
unset($controller->data[$model][$key]);
}
}
}
$controller->Session->write($controller->name.'.'.$controller->params['action'], $controller->data);
}
$filter = $controller->Session->read($controller->name.'.'.$controller->params['action']);
$controller->data = $filter;
}
}
?>
How to use it:
1. Add this code into filter.php file under your /app/controllers/components
2. Add in your controllers which require filtering:
class CountriesController extends AppController {
var $name = 'Countries';
...
var $components = array(...,'Filter');
function index() {
$this->Country->recursive = 0;
$filter = $this->Filter->process($this);
$this->set('countries', $this->paginate(null, $filter));
}
}
?>
That’s it now your filter forms will keep the values until you delete them or set a specific JavaScript function which would be “clear all fields and submit” – I leave it to your imagination
Note that in the component there is an array which store all wrappers for different type of data, i.e. strings will be transformed with …field LIKE ‘filter_string%’ etc. you can modify this in the component directly, or by setting the custom array before using the prepare function.
Well, that’s it. nothing special, but doing miracles
I would appreciate if you share your approach and ways to filter data.


well, thank you again.
little remark, for thouse which allready startet with filter story, and alread have a form.
// needed an hour to find
in view
> $form->create(‘Country’, array(‘action’=>’/index’));
where ‘Country’ is a correct Model name you using.
so FilterComponent can find needed Fieldstype.
I didn’t understand you completely, but I mention in the beginning – get the form from your Add action
Sweeeet
is there any way to store the sort and direction variable from $paginate as well? This seems to get lost after ie deleting or editing an entry.
Thanks a lot mate
wirtsi
Wirtsi, great idea and I think it’s easy to be implemented. I am going to work on it these days.
Nice concept – will be more automagical if you create a beforeFilter instead of process and set filter directly from your component into the controller. All that the user will need to do then would be $this->paginate(null, $this->filter)
Tarique Sani – Good idea but, it would be more complex, because in the beforeFilter function you need to determine the index submission, from insert or edit submit. Otherwise it’s true, if you set it in beforeFilter function in AppController it will be very elegant
Determining index submission is easy – $this->action has the name of the current action
with multiply select box did’t work =(
Hello, I’ve changed the returned array this way:
if(isset($this->fieldFormatting[$columns[$k]])){
$ret[$key.'.'.$k] = sprintf($this->fieldFormatting[$columns[$k]], $v);
} else {
$ret[$key.'.'.$k] = $v;
}
And
var $fieldFormatting = array(
“string”=>”LIKE %%%s%%”,
“date”=>”‘%s’”
);
because I got an sql error in some case (colname ambiguos). Now the query is something like
(single field filter, using latest core version -7008- from svn)
.. WHERE ‘ModelName.colname’ LIKE ‘%enteredstring%’
instead of
.. WHERE ‘colname’ LIKE ‘enteredstring%’
I don’t know if it is always correct this way, but it works for me
anyone getting:
Warning: Call-time pass-by-reference has been deprecated – argument passed by value; If you would like to pass it by reference, modify the declaration of [runtime function name](). If you would like to enable call-time pass-by-reference, you can set allow_call_time_pass_reference to true in your INI file. However, future versions may not support this any longer. in /home/htdocs/app/controllers/components/filter.php on line 38
in function process(&$controller) {
line38: $this->_prepareFilter(&$controller);
then it doesn’t work…
This is great. I’ve changed the code slightly, now it also supports associations with a different name:
function process(&$controller){
$this->_prepareFilter(&$controller);
$controllerModelName = Inflector::singularize($controller->name);
$controllerModel = $controller->{$controllerModelName};
$associated = $controllerModel->getAssociated();
$ret = array();
if(isset($controller->data)){
//Loop for models
foreach($controller->data as $key=>$value){
$modelName = $key;
if(!isset($controllerModel->{$key}) && isset($associated[$key])){
$assoc = $associated[$key];
$modelName = $controllerModel->{$assoc}[$key]['className'];
}
if(isset($controllerModel->{$modelName})){
$columns = $controllerModel->{$modelName}->getColumnTypes();
foreach($value as $k=>$v){
if($v != ''){
//Check if there are some fieldFormatting set
if(isset($this->fieldFormatting[$columns[$k]])){
$ret[$key.'.'.$k] = sprintf($this->fieldFormatting[$columns[$k]], $v);
} else {
$ret[$key.'.'.$k] = $v;
}
}
}
//unsetting the empty forms
if(count($value) == 0){
unset($controller->data[$key]);
}
}
}
}
return $ret;
}
</pre>
Really great component! I have made some changes so I don’t have to put filter data into session. You can see my code at http://blog.uplevel.pl/index.php/2008/06/cakephp-12-filter-component/ . Thanks a lot!
Thanks all for your comments
I personally prefer to have filter stored in a Session, butcause it’s “stored” until you leave the browser, or you logged out.
@Maciej – nice approach, but It’s a matter of taste.
I am glad that this helps
thanks youu.
Just a tiny change to let the component work with cake 1.2 RC – (move the operator to the “key” side)
$this->_prepareFilter($controller);
$ret = array();
if(isset($controller->data)){
//Loop for models
foreach($controller->data as $key=>$value){
if(isset($controller->{$key})){
$columns = $controller->{$key}->getColumnTypes();
foreach($value as $k=>$v){
if($v != ''){
//Check if there are some fieldFormatting set
if(isset($this->fieldFormatting[$columns[$k]])){
$ret[$key.'.'.$k .' LIKE '] = sprintf($this->fieldFormatting[$columns[$k]], $v);
} else {
$ret[$key.'.'.$k .' LIKE '] = $v;
}
}
}
//unsetting the empty forms
if(count($value) == 0){
unset($controller->data[$key]);
}
}
}
}
return $ret;
}
@Stefano – thanks! I haven’t used this component with 1.2RC so far. I will change in the core right now.
Not returning correct result. Due to `$fieldFormatting` has “LIKE” defined in it, then again in `process()`:
$ret[$key.'.'.$k .' LIKE '] = sprintf($this->fieldFormatting[$columns[$k]], $v);
So the resulting query is (in my example):
WHERE `Photographer`.`first_name` LIKE ‘LIKE Bre%’
Yeah, that’s wrong …
Small, easy fix.
@Chris,
Except it doesn’t retain the current controller model’s fields … if the current controller is dealing with ModelA that has an association to ModelB … your code seems to setup ModelB’s stuff, but forgets about ModelA’s.
Nik, You said this:
‘That’s it now your filter forms will keep the values until you delete them or set a specific JavaScript function which would be “clear all fields and submit” – I leave it to your imagination’
BUT, my imagination is NOT very GOOD
Can you (or someone) post some code that will reset the session variables (or another way) to remove the filter that can be assigned to a separate button.
Johnny,
Clearing the filter is simply submit an empty form. If the form has values in the text fields you need to go and manually delete their values. Of course if you have a javascript function attached to onclick event to a button could reset them very easy.
Here is a simple example with 2 text fields could be:
document.getElementById('field1').value = '';
document.getElementById('field2').value = '';
form.submit();
}
after this you attach this onclick event like:
Of course, it could be more elegant and common including reset of select etc, but this one is a quick and dirty example.
I built a reporting system… It’s quite different. It does not use sessions, however I do need to rewrite and optimize it for the changes in RC3..
https://sites.google.com/a/esninteractive.com/cakephp-documentation/extensions/cake-reporting-filtering-extension
-d
I built a reporting system… It’s quite different. It does not use sessions, however I do need to
http://www.turkyoutube.org/populer.php