Stop ACTA

CakePHP generic filter plugin

Posted in CakePHP on 02.01.2010.

Although cake takes good care of the basic CRUD functionality, I often need to do one more thing with my data. And that's filtering it. Or "searching", if you like it more that way.

What I hate about it is the fact that it's not there by default. I was working on an analytical app at work and our client demanded a search feature. We're talking about loads of medical data, and more often then not, you don't want to see everything. So, instead of writing a search feature for all our models, I decided to create a plugin which would do the dirty work for me; so I could reuse it in the future.

One thing bothering me, was pagination, sorting and search combined, which obviously has to work. Although it's not pretty, I managed to get that working too. Not only that, I've also added "persistence", the ability of this plugin to remember your search when you leave the page and return later (in the same session of course, but I'm sure you can easily serialize plugin data to work as a user preference).

The idea here is to get things working fast, but reserving some flexibility for the future, if the need arises for a more complex filtering options (more about that later). Basically, if you just want to slam a "search form" on top of your data, you can do that in a few minutes. If you need something more complex, it's not there by default, but it can be easily implemented.

Please note that this plugin was written hastily, so the code is not something I'm proud of. But, it works for our app perfectly, so I'm not changing it - for now. If you have suggestions or bugfixes, let's hear them!

Basic usage

To use this plugin, you need to include the plugin's Filter component just like any other. For the sake of this example, let's say you have a Document model, and it belongs to a User model. It also has a text description, a year (integer) and a user id. To enable filtering for your index action, this is what you do:

class DocumentsController extends AppController
{
    var $components = array('Filter.Filter');

    var $filters = array
        (
            'index' => array
            (
                'Document' => array
                (
                    'Document.description',
                    'Document.year' => array('condition' => '='),
                    'Document.user_id' => array
                    (
                        'type' => 'select',
                        'label' => 'Document owner',
                        'selector' => 'getOwnerList'
                    )
                )
            )
        );
}

First some details. Document description is a text field, and the filter will take all the defaults for this field, it will output a text box, and the search operator is LIKE %your_text%.

The second input, year, will also be a text input, but we want the user to be specific here, search operator is "=".

The third field has type set to 'select', which obviously means it will be a select box (dropdown list, or whatever you call it..). Since we don't want our users to see a label "User Id", we tell the plugin that the label text should be "Document owner".

Additionally, since some users can't be document owners, we'll give the plugin a custom function which will return a list of potential owners. That's the 'selector' parameter, in this case a method of the Document model, getOwnerList(). Of course, you can just let the plugin deal with the default, and it will acquire the Document model and call Document::find('list', ...).

That's all you need to do in your controller. Now, since you said plugin needs to use Document::getOwnerList(), you better have one ready. What the function does is irrelevant. What matter is that it needs to return data for Cake's FormHelper to use (for $form->select()). So once you sort that out, you're ready to show your search form to the world: open /views/documents/index.ctp and add this where you want the search form to be:

// for cake 1.3.x
echo $filter->filterForm('Document', array('legend' => 'Search'));
// for cake 2.x
echo $this->Filter->filterForm('Document', array('legend' => 'Search'));

What it does is simply outputs everything we said above, for the Document model, with all the fields and the submit button.

You may have noticed that we didn't add the Filtered behaviour to our model, or add the FilterHelper anywhere. That's because the component takes care of that.

Now, since the Filtered behaviour is added to the model, it will filter your data on any query you do in this action. Since that may not be what you always want, there is an option to switch it off for the current query, just add the 'nofilter' param:

$this->Document->find('all', array('nofilter' => true));

OK, that's our simple usage example. With this code you can search, paginate, go to other pages in your app to destroy data at random, return to this page and your search will be remembered. Of course, sometimes you don't want that. You can switch it off as well:

var $components = array
    (
        'Filter.Filter' => array
        (
            'nopersist' => array('index')
        )
    );

There are of course some other options for the plugin:

var $filters = array
    (
        'index' => array
        (
            'YerModel' => array
            (
                'YerModel.some_bool_value' => array
                (
                    'type' => 'checkbox',
                    'default' => true
                ),
                'RelatedModel.some_field' => array
                (
                    'type' => 'select',
                    'selectOptions' => array
                    (
                        'order' => 'RelatedModel.arse ASC'
                        /* other options too.. */
                    ),
                    'required' => false,
                    'label' => 'Feck, Drink?',
                    'filterField' => 'YerModel.related_model_id'
                ),
                'YerModel.craggy_island' => array
                (
                    'condition' => 'like'
                    /*
                    can also be: = < > like => > < =<
                    */
                )
            )
        )
    );

Advanced usage

Advanced usage includes using Model callbacks (beforeDataFilter() and afterDataFilter()), creating custom fields in the search form, handling their input etc.. If there's interest in that, let me know and I'll do my best to write some examples in another article.

Update 21/04/2011: About that, here it is: CakePHP Filter Plugin advanced usage

Code

The code is available at GitHub:

Again, please note that this was a hastily written plugin and the code is not nice or optimal. I'd like to hear your ideas and suggestions for some ways to improve the plugin and make it more generic and more extendable.

If you have trouble using the plugin, drop me a message here, on GitHub or simply on <neutrinocms AT gmail>, I'll do my best to help you out.

Happy baking!

Article comments — View · Add


Page 1 of 16
1 · 2 · 3 · 4 · 5 · 6 · 7 · 8 · 9

Sacha :: 22.01.2010 03:37:06
I've had an error saying the class 'Sanitize' wasn't found.
(using cake 1.2)

I fixed it by adding:
App::import('Core', 'Sanitize');
in plugins/filter/models/behaviors/filtered.php @ line 170

Is this correct procedure ? Or is there a better fix ?
Sacha :: 22.01.2010 03:45:06
Is it possible to add 2 date search options, for 1 date field ?

To able to search between 2 dates, 'range between'...

fe: Find all where Docu.date >= lastweek AND Docu.date <=yesterday

Can this be implemented ?
lecterror :: 22.01.2010 05:20:06
Hi Sacha!

It is possible to add that kind of filtering, but it is not there by default.

The "right" way to implement it would be to use beforeDataFilter() and/or afterDataFilter() callbacks provided by the plugin to modify your search query (basically, you provide fake input fields, parse their input and convert it to a find() condition in those two callbacks). Maybe I should write that "advanced usage" guide after all. :-)

Also, thanks for telling me about the missing import, I've corrected it in the repo.
Agota Perez :: 25.01.2010 02:50:13
Hi there,
Thanks for a wonderful plugin, I am using it on my current project and it works like a charm. I was just wondering there is a simple way to do a 'reset' to clear the form data? I tried '<button type="submit" name="data[reset]" value="reset">Reset</button>' but it doesn't clear the form. Any help will be much appreciated.
lecterror :: 25.01.2010 15:09:56
Hi Agota!

The thing about reset buttons is that they reset the form to their *default* values, those encoded in HTML itself. You can use the non-default methods of the plugin helper (beginForm, inputFields, endForm) to generate your search form and "inject" your own button before calling endForm(). With that button you can reset the form (probably by using javascript and submitting the form).

Let me know if you need any help, email me and I'll try to answer as soon as I'm able to.

Cheers!