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

Archive July, 2010

[Cake on Steroids] Write-once Autocomplete Search

31 July, 19:34, by zehzinho

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.

[Cake on Steroids] PHP + JS

16 July, 18:29, by zehzinho Tags: , ,

Hi, today I’m starting a series of posts with tips on how to improve your CakePHP productivity even more. By productivity I mean: doing things faster and/or in a more organized way. I’m calling it “Cake on Steroids”. As I searched the web and could not find anything like this, I think it’s ok to use this name, right? Enough said, let’s start.

Well, one thing that really annoyed me since the first time I used CakePHP was the way Javascript is handled per default by the framework. It’s not hard to find views with stuff like this:

$javascript->codeBlock(
  'window.addEvent("domready", function(){alert("hi there);});',
  array("inline" => true) );

The two main problems of writing JS code this way are:

  1. No editor can provide javascript syntax highlighting in PHP strings, right? This is a nightmare.
  2. Javascript mixed with PHP and HTML code.

We all know that the second reason can easily be solved in CakePHP, since the framework, obviously, allows us to use JS files. But there are some situations in which it would be nice to be able to use some PHP code within the Javascript code. For example, sometimes we use URLs in the javascript code (in an autocomplete function, for example) and the url() function from the Html helper is nice.

Another strong reason to use PHP + JS is to maintain consistency between elements in the view and the Javascript. What kind of elements? Images and, even more importantly for those who need it, translations. We just don’t want to have to translate things twice (one using cake and another using JS), right? Even if we could use the same translation file for PHP and JS, it would be nice to have elements attached to only one keyword, so we reduce the chances of making mistakes (eg. changing the keyword only in the PHP or only in the JS).

Well, as I said, all this annoyed me. And I think most people can figure out more scenarios in which it would be nice to have PHP + JS in their environments. So, the good news are that it is perfectly possible to do it in CakePHP.

I have found some articles which teach how to do it. Oh, I forgot to say. I’m using this solution for quite some time and I’ve been postponing this post for months and now I just couldn’t find my original sources. If I find them, I’ll update the post, ok?

So, how do we do it?

The first step is to tell cake to parse the js extension. You can do it by editing app/config/routes.php and adding the following statement:

Router::parseExtensions('js');

The second step is to add a default layout for JS. You can do it by creating and editing the file app/views/layouts/js/gen.ctp:

<?php
 
header("content-type: application/x-javascript");
 
echo $content_for_layout;
 
$this->data = null;
Configure::write('debug', 0);
?>

The third and last step is to create an action in a controller which will be responsible for loading our JS file. I like to add this action to my AppController, so I don’t have to write it in all my controllers. Edit you AppController and add the following function:

 
 public function formjs($id) {
    if ($this->params['url']['ext'] != 'js') {
        exit;
    }
 
    $this->layout = 'gen';
    $this->ext = '.js';
 
    $this->set("cacheDuration", '1 hour');
  }

Adding this function to my AppController allows me to add the following lines to my default layout (usually app/views/layouts/default.ctp):

$autoloadFormJs = APP . 'views' . DS . this->params["controller"] . DS . "js" . DS . "formjs.js";
 
$urlAction = $this -> params['action'];
 
if (($urlAction == 'add' || $urlAction == 'edit') && is_file($autoloadFormJs)) {
      $jsPath = '/' . $this->params["controller"] . '/formjs/1';
      echo $javascript->link(array($jsPath));
}

and voilá. All you have to do is create a js/formjs.js file inside the view folder of your component and it will be inserted automatically. For example, if you have a component called bodies, and you want some javascript to be automatically loaded for the add and edit actions, all you have to do is create the file views/bodies/js/formjs.js and the code above will insert the file automatically in the page.

Of course, I’m just trying to speed up your development process ;) But you can be more conservative and place the formjs function only in the bodies controller. Or you can load the javascript only in the add.ctp view of the bodies component. To do it, you would have to place following line in your views/bodies/add.ctp file:

echo $javascript -> link(array('/bodies/formjs/1'));

A positive side-effect of this approach is that you can pass the id argument in the javascript URL. In the examples above, all links to javascript files use the id = 1. But you can use different values for the id and the javascript file will be loaded the same way. This is actually a feature of this approach, since you can use aggressive caching policies for your javascript files and change the id whenever the file is modified, assuring consistency.

Now our javascript files are being loaded by the CakePHP framework and we can use PHP inside of them in anyway we want.

I usually write the PHP code inside javascript comments, so the editor does not give me syntax errors. For example:

/*<?php
$usersAutocompleteUrl = $html->url(array('controller' => 'users', 'action' => 'autocomplete'));
*/
 
$(function() {
  // using the previously defined PHP variable
  $('#userSearchField').autocomplete('<?= $usersAutocompleteUrl ?>', {
    // lots of JS code
  });
});

Finally, I created a helper called PhpInJS to help me define variables in the PHP view which can be used in a JS file. Place the following code in the views/helpers/php_in_js.php file:

class PhpInJsHelper extends AppHelper {
  function set($name, $value) {
    if (!empty($this->params['controller'])) {
      $controller = $this->params['controller'];
      $_SESSION['PhpInJs'][$controller][$name] = $value;
      return $value;
    }
 
    return null;
  }
 
  function get($name) {
    if (isset($_SESSION['PhpInJs'][$this->params['controller']][$name])) {
      return $_SESSION['PhpInJs'][$this->params['controller']][$name];
    }
  }
}

You can use this helper in the view file (eg. add.ctp) to set a variable:

$eraseImage = $phpInJs -> set('eraseImage', $html -> image('erase.png'));
 
// use the $eraseImage var in the PHP file

and use the the same value in the JS file:

/*<?php
$eraseImage = $phpInJs -> get('eraseImage');
?>*/

Well, I think it’s enough. You can/should always use the comments to help me improve/fix the solution, ok? See ya.

Welcome to the new version of my blog

07 July, 15:07, by zehzinho Tags:

Hi, after some experiences with wordpress and drupal, I decided to switch back to WordPress. I hope you all enjoy this new version. I’m planning to use this new version of the blog to expose more of my technical discoveries, specially regarding web development.

Despite the fact that I’m Brazilian, the default version of the blog is the English one, because I want to keep myself practicing the few English words I know. Of course, sometimes there will be posts that will concern only Brazilian people, and I will post only the Portuguese version.

Well. I think it’s enough for ANOTHER welcome post. See ya.

Before your next meeting, just think about it

05 July, 15:15, by zehzinho Tags:

“When you think about it, the true cost of meetings
is staggering. Let’s say you’re going to schedule a meet-
ing that lasts one hour, and you invite ten people to at-
tend.
That’s actually a ten-hour meeting, not a one-hour
meeting. You’re trading ten hours of productivity for
one hour of meeting time. And it’s probably more like
fifteen hours, because there are mental switching costs
that come with stopping what you’re doing, going
somewhere else to meet, and then resuming what you
were doing beforehand.”

Jason Fried & David Hansson, Rework