View this site in English Ver este site em português

[Cake on Steroids] Write-once Autocomplete Search

Hi. Tired of having to write code for searches, usually autocompletes, over and over again? If you expect JSON as the outcome of your searches, like the JQuery UI’s Autocomplete does, you may find this post interesting.

In my projects, I add the following function to my AppController to provide a fast search action to all my controllers:

function search() {
     $this->disableCache();
 
     $items = array();
     $fields = explode(',', $this -> params['named']['fields']);
     $model = $this -> modelClass;
 
     $conditions = array();
 
     if (isset($this -> params['named']['conditions'])) {
       $conds = explode(',', $this -> params['named']['conditions']);
 
       for($i = 0; $i < count($conds); $i += 2) {
         $conditions[] = array("$model.{$conds[$i]}" => $conds[$i+1]);
       }
     }
 
     $get = array();
 
     if (isset($this -> params['named']['get'])) {
      $get = explode(',', $this -> params['named']['get']);
     }
 
     if(isset($this -> params['url']['term']))
      {
        $id = (string) strtolower($this -> params['url']['term']);
 
        $selector = "%$id%";
 
        if (count($fields) == 1) {
          $conditions[] = array("LOWER($model." . $fields[0] . ') LIKE' => $selector);
        }
        else {
          $or = array();
 
          foreach($fields as $f) {
            $or[] = array("LOWER($model.$f) LIKE" => $selector);
          }
 
          $conditions[] = array('OR' => $or);
        }
 
        $args = array('conditions' => $conditions);
 
        if (count($get) > 0) {
          // the id must come automatically
          $get[] = "$model.id";
          $args['fields'] = $get;
        }
 
        $this -> {$model} -> recursive = 0;
        $items = $this -> {$model} -> find('all', $args);
      }
 
      $this -> set('items', $items);
 
      $this -> autorender = false;
      $this->viewPath = '/elements';
      $this -> render('search', 'json');
  }

(The original code has been updated. Thanks to: dogmatic69 and Cauan Cabral)

Besides the search action. You also have to have a json.ctp file in your view/layouts dir with the following content:

<?php
echo $content_for_layout;
?>

Now you’re ready to have it working. All you have to do to finally “enable” the feature is to place the view (search.ctp) in your views/elements dir:

<?php
echo  $javascript -> object($items);
?>

Using the search

Currently, the search function accepts the following three named parameters:

  • fields: the (comma separated) fields which we are going to compare against the term parameter.
  • conditions: (pairs of comma separated) conditions for the search
  • get: which (comma separated) fields we want to receive from the search (the result)

Now suppose you have a Post model in your application. If we want to:

  1. search for posts with “php” (term parameter) in the title or in it’s description (fields)
  2. the posts must be published already and the author has the user id (author_id field) equals 4.
  3. we want to retrieve only the title and tags fields

To make the previous search, we can now use the following URL to receive our JSON result:


http://example.com/posts

/search/fields:title,description
/conditions:published,1,author_id=4
/get:title,tags
/?term=php

I think you got it, right? As always, I’m waiting for your comments. See ya.

16 Responses to “[Cake on Steroids] Write-once Autocomplete Search”

  1. [...] This post was mentioned on Twitter by Juan Basso, Zé Ricardo. Zé Ricardo said: [Cake on Steroids] Write-once Autocomplete Search: http://www.josericardo.eti.br/2010/07/31/cake-steroids-write-once-autocomplete-search [...]

  2. dogmatic69 says:

    cool idea but there are some things you could change in your method to make it more “cakeish”, like not using $_REQUEST but rather $this->params['named'] etc

    and you could pick up problems with using $this->modelNames as there could be more than one model loaded on the controller and the one needed may not be the [0] model that is needed. its better to use $this->modelClass

    you can also use Router::parseExtentions(‘json’) with a json view class to make it more automatic, where you can just add .json to the url and the json view takes over delivering the data.

    • zehzinho says:

      Nice tips dogmatic, I’m going to update it ASAP.

    • zehzinho says:

      UPDATED.

      I knew I could parse the JSON extension, but I’m almost sure the JQuery UI’s Autocomplete does not allow me to append the .json string to the generated URL. I’ll check it out ASAP.

      • dogmatic69 says:

        well ive noticed that so long as you have a .json anywere in the url cake picks it up… i have urls like /controller/action/someparam.json /controller/action.json/param:123 /controller/action/someparam/.json

        and they all work

      • dogmatic69 says:

        another random bit, mysql is not case sensitive for the most part so LOWER() is not needed… there are encodings that are case sensitive though.

  3. Cauan Cabral says:

    Nice post…

    About the last step (rendering results in json format), you can use a element (named “search.ctp”) and before call $this->render(‘search’, ‘json’); you set $this->viewPath = “/elements”;

  4. gabriel says:

    This looks like it would be incredibly useful. For someone that’s still relatively new to CakePHP and early into the learning curve, could you provide an example of how to implement this in the view with the form that the user would be typing in? It would be greatly appreciated!

    • gabriel says:

      Looks like I was able to get things working. Ran into a few problems with the newest version of JQuery UI Autocomplete (1.8.2). It is now looking for a ‘label’ field to show the user in the drop down list. In my case, I don’t have a field in my table called label, so I had to create a $virtualFields entry in my model.

      Also, the way the data comes back from the $this->{$model}->find(‘all’,$args); doesn’t mesh well with the format the JQuery UI Autocomplete is expecting:

      [ { "id": "1", "label": "European Robin", "value": "European Robin" }, { "id": "2", "label": "Rufous-Tailed Scrub Robin", "value": "Rufous-Tailed Scrub Robin" } ]

      vs CakePHP’s:

      [ { "Bird" : { "id": "1", "label": "European Robin", "value": "European Robin" }}, { "Bird": { "id": "2", "label": "Rufous-Tailed Scrub Robin", "value": "Rufous-Tailed Scrub Robin" }} ]

      I wasn’t sure if there was a better method to do reformat the data for JQuery, but this is what I ended up using:

      $items = $newItems = array();
      … do all the work …
      foreach($items as $item) {
      array_push($newItems,$item[$model]);
      }

      $this->set(‘items’, $newItems);

      • zehzinho says:

        Gabriel, I’ve only used the JQuery UI’s Autocomplete with a callback as the source, setting the label and value in javascript :) IMHO, returning the whole objects gives us more flexibility.

        • gabriel says:

          zehzinho: I’d like to do this the right way, but am a beginner with jquery as well so am struggling through this. Would it be possible for you to provide an example of a $(‘#blah’).autocomplete({…}) that you’d use to parse through the data as its returned via your search() function above?

Leave Comment