21 Feb

Turn your normal CakePHP forms to AJAX forms in a second

Best Practices, CakePHP, jQuery

CakePHP helpers are so powerful that when you begin to hack helpers here and there you discover a lot of possibilities which can save you a lot of time. Same thing happened when I had an idea to AJAX'ify the forms using some shortcut.

Let's try hacking the FormHelper and enable AJAX. We'll use jQuery Form plugin to submit forms via AJAX request (I don't know any other good plugins for forms).

What we'll be doing:

Converting a normal form like below one, to make it call AJAX(using jQuery lib) and alert us when form posted:

PHP:
  1. <div class="jerks form">
  2. <?php echo $form->create('Jerk');?>
  3.     <fieldset>
  4.        <legend><?php __('Add Jerk');?></legend>
  5.     <?php
  6.         echo $form->input('name');
  7.     ?>
  8.     </fieldset>
  9. <?php echo $form->end('Submit');?>
  10. </div>

 

1. Extend your FormHelper to something like AjaxFormHelper (Now normally I don't extend core helpers like this, but use a similar method similar to this (comment by grigri): http://cakebaker.42dh.com/2008/10/18/dont-abuse-the-apphelper-to-extend-the-core-helpers/#comment-110708 ) and overwrite create() method like this:

PHP:
  1. function create($model = null, $options = array())
  2. {
  3.     $output = "";
  4.     if(isset($options['ajax']) && $options['ajax']=='true')
  5.     {
  6.         if(!isset($options['id']))
  7.         {
  8.             $options['id'] = 'form' . intval(mt_rand());
  9.         }
  10.        
  11.         $this->ajaxForm = $options['id'];
  12.         $url = "$('#".$options['id']."').attr('action')+'?ajax=1&flash_only=1";
  13.        
  14.         if(@$options['response']=='inline')
  15.         {
  16.             $datatype = 'text';
  17.             $success = "$('#".$options['id']."_status').hide().html(responseText).fadeIn();
  18.                         setTimeout(function(){
  19.                             $('#".$options['id']."_status').fadeOut();
  20.                         }, 5000);";
  21.             $url .= "&js=false'";
  22.         }
  23.         else {
  24.             $datatype = 'script';
  25.             $success = '';
  26.             $url .= "'";
  27.         }
  28.        
  29.         // to-do: avoid multiple inclusion of this script
  30.         $output .= "<div id='".$options['id']."_status' style='display:none;'></div>";
  31.         $output .= "<script src='".$this->Html->url('/effects/js/jquery.form.js')."'></script>";
  32.         $output .= "<script>
  33.         $(document).ready(function() {
  34.         $('#".$options['id']."').ajaxForm({dataType: '".$datatype."', url:  ".$url.",
  35.             beforeSubmit: function(){
  36.             $('#".$options['id']." .submit input').attr('disabled', true);
  37.             $('#progressIndicator').show();
  38.             $('#".$options['id']." .form_progress').show();
  39.             },
  40.             success: function(responseText, statusText){
  41.             $('#".$options['id']." .submit input').attr('disabled', false);
  42.             $('#progressIndicator').hide();
  43.             $('#".$options['id']." .form_progress').hide();
  44.             ".$success."
  45.             },
  46.         });
  47.     });
  48.     </script>";
  49.     }
  50.     // unset js options
  51.     $output .= parent::create($model, $options);
  52.    
  53.     return $this->output($output);
  54. }

I'm not that good in explaining code flow, but you might have noted 3 get variables appended to our form action URL above. These variables are: 'ajax','flash_only' ,'js' and serves their own purpose that I'll tell in next steps.

 

2. Now we enable the AJAX in our form (CakePHP's $options array is so good that you can overwrite and modify almost many methods easily):

This will show a JS alert after form has been submitted successfully.

PHP:
  1. <div class="jerks form">
  2. <?php echo $ajaxForm->create('Jerk', array('ajax'=>'true'));?>
  3.     <fieldset>
  4.        <legend><?php __('Add Jerk');?></legend>
  5.     <?php
  6.         echo $ajaxForm ->input('name');
  7.     ?>
  8.     </fieldset>
  9. <?php echo $ajaxForm ->end('Submit');?>
  10. </div>

This will show an inline message after form has been submitted successfully.

PHP:
  1. <div class="jerks form">
  2. <?php echo $ajaxForm->create('Jerk', array('ajax'=>'true' , 'response'=>'inline'));?>
  3.     <fieldset>
  4.        <legend><?php __('Add Jerk');?></legend>
  5.     <?php
  6.         echo $ajaxForm ->input('name');
  7.     ?>
  8.     </fieldset>
  9. <?php echo $ajaxForm ->end('Submit');?>
  10. </div>

Note that ajax=true parameter we sent in create() method. This will tell helper to load this form via AJAX.

 

3. Now your form will be AJAX ready (if you've included this AjaxFormHelper properly). But because our controller function was made to process normal POST function, and flash message on success – we'll have to change that behavior. This is what a normal JerksController::add() method should look like:

PHP:
  1. function add() {
  2.         if (!empty($this->data)) {
  3.             $this->Jerk->create();
  4.             if ($this->Jerk->save($this->data)) {
  5.                 $this->Session->setFlash('Jerk saved.');
  6.             } else {
  7.             }
  8.         }
  9.     }

We don't need full action content from views/jerks/add.ctp to appear in response when AJAX is called. We can make this work traditional way by checking if it's an AJAX request in controller method itself and do needful, but I wouldn't want to modify all my controller functions to enable AJAX, so here's what I've come up with.

Remember the 3 GET variables above?

'ajax' => This one will determine if a given HTTP request is an AJAX request.

'flash_only' => This will tell if rendering should happen or not. Flash only means, after controller function is executed, do not render, just show flash message.

'js' => This is used for alert type, if this is not set, show inline alert. If set true, helper must show JS alert() on form success.

Inside your AppController::beforeFilter(), add this code:

PHP:
  1. if(isset($_GET['ajax']))
  2. {
  3.     Configure::write('debug',0);
  4.     $this->layout = 'ajax';
  5.     $ this ->set('ajax', true);
  6.     if($_GET['flash_only'])
  7.     {
  8.         $ this ->set('flash_only', true);
  9.         //$ this ->autoRender = false;
  10.     }
  11.    
  12.     if($_GET['js']=='false')
  13.     {
  14.         $ this ->set('js', 0);
  15.     }
  16.     else {
  17.         $ this ->set('js', 1);
  18.     }
  19. }

Do not blame me for using GET variables, I found many issues with 'named' so I'm relying on normal GET variables.

Now you'll need to edit ajax.ctp layout file.

PHP:
  1. <?php
  2. if ($session->check('Message.flash'))
  3. {
  4.     $strMessage = '';
  5.     $message = $session->read('Message.flash');
  6.     if(isset($message['params']['type']))
  7.     {
  8.         $type = $message['params']['type'];
  9.         $strMessage = ucfirst($type).": ".$message['message'];   
  10.     }
  11.     else {
  12.         $strMessage = $message['message'];     
  13.     }
  14.     $session->del('Message.flash');
  15. }
  16. if(!empty($this->validationErrors))
  17. {
  18.     $strMessage = '';
  19.     foreach($this->validationErrors as $model=>$errors)
  20.     {
  21.         foreach($this->validationErrors[$model] as $field=>$error)
  22.         {
  23.             $strMessage .= $error;
  24.         }
  25.     }
  26. }
  27. ?>
  28. <?
  29. if($strMessage) {
  30.     if($js==1) {
  31. ?>
  32.     alert('<?=$strMessage;?>');
  33. <? } elseif($js==0) {
  34.     echo $strMessage;
  35. } } ?>
  36. <?
  37. if(!isset($flash_only))
  38. {
  39.     echo $content_for_layout;
  40. }
  41. ?>

This should be self explanatory, even though this code definitely needs some refactoring. We're basically manipulating those 3 GET variables according to our need. In process, we're also checking validation errors occurred in form.

I have not used it under the production environment yet, so I'd really like to hear any pitfalls (if any) using this approach. Thanks for reading.

Abhimanyu Grover

30 Oct

How to keep your database under version control?

Best Practices, CakePHP

I've been looking for a solution to this problem from quite a few days now, and I did find it quite interesting that there's no standard way to do this. So I decided to ask the CakePHP community, while there are many tools available to do the things, but I'm going to share the best solution of all, which is also Cake based. CakePHP has something known as Schema shell, which helps you solving the problem in efficient way.

Let me describe the problem first briefly. There was this comic I saw few days back (can't find it right now), which inspired me to solve this problem. Here goes the text from that cartoon:

 

Alex: Ok, we're ready, let's sync our work on this project today to show to client.

Rob: Ok sure, let me see the database changes and put it to server.

Alex: Oh yea, let me do it too.

Rob: Hey, I made this change, is it yours? And what about this? Where are you using it?

Alex: Yes-No-Yes-….

Boss: And we're screwed.

 

That was happening all the time with our team too, before I found this way.

Here's how it works:

1. Setup

Assuming that you already have a working project to implement db versioning on, start command line console from your 'app' directory. And then run 'cake schema help' to make sure you have the shell, or for necessary instructions.

 

2. Generate first Schema

After you see it working fine, lets output our whole DB structure into a dump file, it's not SQL dump file, its schema file which is in Cake-friendly format. You can play with other commands like 'schema view' to make sure things works. Ok, let's generate our schema with 'cake schema generate'.

Ok, schema.php generated, it would be inside your app/config/sql

Here's how the file will look:

PHP:
  1. <?php
  2. /* SVN FILE: $Id$ */
  3. /* App schema generated on: 2008-10-30 23:10:37 : 1225410517*/
  4. class AppSchema extends CakeSchema {
  5.     var $name = 'App';
  6.  
  7.     function before($event = array()) {
  8.         return true;
  9.     }
  10.  
  11.     function after($event = array()) {
  12.     }
  13.  
  14.     var $users = array(
  15.             'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),
  16.             'name' => array('type'=>'string', 'null' => false, 'length' => 25),
  17.             'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1))
  18.         );
  19. }
  20. ?>

Looks good (with the sample db). Now as you have a file for the whole database, you can put it in a repository under version control along with your project.

 

3. Update Schema

Since, there will be changes in database throughout the development of the project, and they will be done by many other developers. We'll need to keep a track of it, to do so, we'll make sure schema files remains updated always – this is something you'll have to instruct your team about.

Let's make a sample change and see how it goes. In my users table, I am going to add a new field called 'password' and then regenerate my schema file.

ALTER TABLE `users` ADD `password` VARCHAR( 20 ) NOT NULL ;

Lets regenerate schema now:

CakePHP's Schema shell had detected the change automatically, and prompted me if I want to over-write the current schema or snapshot it (create a new one). In my case, I want to overwrite it as I already have it in version control. Let's see what new schema looks like:

PHP:
  1. <?php
  2. /* SVN FILE: $Id$ */
  3. /* App schema generated on: 2008-10-30 23:10:33 : 1225411053*/
  4. class AppSchema extends CakeSchema {
  5.     var $name = 'App';
  6.  
  7.     function before($event = array()) {
  8.         return true;
  9.     }
  10.  
  11.     function after($event = array()) {
  12.     }
  13.  
  14.     var $users = array(
  15.             'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),
  16.             'name' => array('type'=>'string', 'null' => false, 'length' => 25),
  17.             'password' => array('type'=>'string', 'null' => false, 'length' => 20),
  18.             'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1))
  19.         );
  20. }
  21. ?>

You see the 'password' field was reflected here as well. Now you can keep your whole DB structure under this schema file with version controlling. However, we've yet to sync the database across different machines. Let's say after updating (svn up) on my local setup, I got a new schema which I would like to implement on my database. Here's how you'll do that in next step.

 

4. Syncing

Let's assume the other user has added a field 'address' in his database, and regenerated the schema. Now I want same change to reflect on my local database. Here's new updated schema looks like (it's only a part of the schema.php file):

PHP:
  1. var $users = array(
  2.             'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),
  3.             'name' => array('type'=>'string', 'null' => false, 'length' => 25),
  4.             'password' => array('type'=>'string', 'null' => false, 'length' => 20),
  5.             'address' => array('type'=>'string', 'null' => false, 'length' => 40),
  6.             'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1))
  7.         );

Now there are normally 2 options to update your database:

  • you drop all tables and then import fresh DB structure from schema.
  • Just update (We need this one)

Here's what you'll do:

And done, our database is now synced. You can run the same process across different machines to never worry about manual syncing. [Please note by 'sync' I mean only the structure not the records.]

Hope you enjoyed it!

 

Abhimanyu Grover

 

07 May

Experimental Searchable Behavior in CakePHP

Best Practices, CakePHP

I always have bad time writing code for search in any new project. So, I decided to write a quick Searchable behavior in CakePHP so as to avoid doing it again and again in any project.

Please note: This only handles text based searches, and it's a very basic version, written in less than two hours.

Here's how it works:

  1. In the model which you want to be searchable, just add:

    PHP:
    1. var $actsAs = array('Searchable' => array());

  2. Now in controller you can use function called Model::stringSearch()

    PHP:
    1. // here your $keyword can be something like ‘nicole scherzinger’
    2. $videos = $this->Video->stringSearch($keywords, array('title', 'description'));

Algorithm behind its functioning (inside Model::stringSearch()):

  1. Search procedure 1: Consider $keywords as phrase, and fetch ids based on this phrase from the passed fields. So if you have passed on :

    $keywords = 'nicole scherzinger';

    $videos = $this->Video->stringSearch($keywords, array('title', 'description'));

    It will be treated as "nicole scherzinger", the results returned here will be marked as highest relevant results.

  2. Search Procedure 2: Find 'nicole' and 'scherzinger' together in given fields. Relevancy --
  3. Search Procedure 3: Find 'nicole' or 'scherzinger'. Relevancy decreased again.
  4. Now it combines all the ids returned from all 3 types of searches, in the same order of their relevancy. Then fetches records using findAllbyId() and returns them to caller.

Download it here. Hope you people like it.

P.S. My girlfriend will be angry when she will see Nicole scherzinger here… So, if you're reading it, I'm sorry I was just working on this MP3 and Videos project, you know?? hehe :)

- Abhimanyu Grover

Hire us

Contact us to get a free quote on your project.