Thursday, June 4, 2009

Auto-Delete Old Data Before Saving A New Record in CakePHP

Thursday, June 4, 2009 4

I think it happens sometimes, that you need to keep a certain amount of data, and don't want your database table to grow out of limit.

For example, we have a fairly typical Post model and a posts table with more than 100 posts. We only want the latest 100 posts in the table, so we need to delete older posts on every new post.

To accomplish this in Cake way, we need to write a bit of script. So, let's add a trimming method to our Post model (You can also add it to AppModel to make it reusable in other models):

<?php
class Post extends AppModel {

    var $name = 'Post';
 
    function trim($options) {
        $defaults = array('conditions' => null, 'maintain' => null, 'order' => null, 'cascade' => true, 'callbacks' => false, 'notices' => true);
        $options = array_merge($defaults, (array)$options);
        extract($options);
        
        if ($notices) {
            $calledFrom = debug_backtrace();
            if ($calledFrom[1]['function'] != 'beforeSave') {
                trigger_error(
                    __("({$this->alias}::trim()) should only be called by Model::beforeSave()", true),
                    E_USER_WARNING
                );
                return false;
            }
        }
        
        $conditions = (array)$conditions;
        $count = $this->find('count', array_merge(array('recursive' => -1), compact('conditions')));
        
        if ($count <= 0) {
            return false;
        }
        
        if (is_int($maintain) && $maintain > 0) {
            $start = $maintain - 1;
            $last = $count - $start;
            unset($maintain);
            
            if ($start == $last) {
                return false;
            }
        }

        if (isset($start) && isset($last) && $count > $start) {
            if (is_null($order)) {
                $order = array($this->alias . '.' . $this->primaryKey => 'desc');
            }
            $limit = $start . ',' .  $last;
            $ids = Set::extract(
                $this->find('all', array_merge(array(
                    'fields' => "{$this->alias}.{$this->primaryKey}", 'recursive' => -1),
                    compact('conditions', 'order', 'limit')
                )),
                "{n}.{$this->alias}.{$this->primaryKey}"
            );

            $this->deleteAll(array($this->alias . '.' . $this->primaryKey => $ids), $cascade, $callbacks);
        }
    }
}
?>

In theory it's pretty simple.

  1. We count to get total number of recods in our posts table along with conditions (if needed)
  2. We get all the IDs of the records to delete.
  3. And then, we delete them with Model::deleteAll().

Now, let's call it in our Post model beforeSave():

function beforeSave() {
    $this->trim(array(
        'maintain' => 100
    ));
    return true;
}

Here are some options available for the trim() method.

$options has the following possible keys - all of which are optional except for the 'maintain' key:

conditions mixed Conditions to match (no default)
maintain integer The maximum number of records to maintain (no default)
order mixed String or array defining order primary key desc
cascade boolean Set to true to delete records that depend on this record true
callbacks boolean Run callbacks (not being used). false
notices boolean When set to false, E_NOTICES wont't be displayed true

That’s all folks.

Friday, May 1, 2009

CakePHP Form Validation with Ajax Using jQuery

Friday, May 1, 2009 29

Data validation, an important part of any application, is useful to ensure that your users enter data properly. When you create forms, you will need to determine which validation strategy to use, client-side, server-side or maybe mixed. Client-side validation has advantages for the server, the user and the developer, but the problem is that anything running on the client end can never be trusted.

Speaking of server-side validation in CakePHP, its built-in validation mechanism provides a powerful, yet easy-to-use environment. However, when it comes to Ajax form submission, the reality is that it involves a lot of technologies. There are already a number of examples on the web on how to implement data validation with the Prototype JavaScript framework and CakePHP's AJAX helper. But so far there are not so many examples of an integration with CakePHP + jQuery.

Now, we'll take a look at how to do Ajax form validation with CakePHP and jQuery.

Before we start, let's set some goals first:

  • We need to ensure that the user enter data properly by using CakePHP's validation class
  • We would like to display an individual error message below each field
  • We would like to display flash messages on success and failure
  • We would like to validate multiple models in a single form view
  • We would like to switch languages based on Localization (shortened to 'l10n')
  • If the data is saved, we want the user to redirect to another location

Whew... That's a lot, isn't it?

Let's start with our typical Post Model with some validation rules.

<?php
class Post extends AppModel {
    var $name = 'Post';
    var $validate = array(
        'title' => array(
            'required' => array('rule' => 'notEmpty'),
            'maxlength' => array('rule' => array('maxLength', 30))
        ),
        'body' => array(
            'required' => array('rule' => 'notEmpty'),
            'maxlength' => array('rule' => array('maxLength', 200))
        )
    );
}
?>

We have two fields, “title” and “body” to be validated. We'll ensure that the fields are not empty, and the data stays within a maximum length requirement.

Let's do a simple view (posts/edit.ctp) which we'll use for both “add” and “edit” actions:

<?php
    if (isset($javascript)) {
        $javascript->link(array(
            'jquery/jquery.min',
            'page_specific/posts_edit'
        ), false);
    }
?>

<div class="posts form">
<?php echo $form->create();?>
    <fieldset>
        <legend><?php $this->action == 'add' ? __('Add Post') : __('Edit Post'); ?></legend>
    <?php
        echo $form->input('id');
        echo $form->input('title');
        echo $form->input('body');
        echo $form->input('published', array('checked' => true));
    ?>
    </fieldset>
    
    <div id="loadingDiv" style="display:none;">
        <?php echo $html->image('ajax-loader.gif', array('alt' => 'Loading...')); ?>
    </div>
 
<?php echo $form->end(__('Submit', true));?>
</div>

We have included the jQuery core and a file that will be specific to this view (No worries, we'll create this file next).

Also, we have added a loading div element with an image inside to handle the loading status of the request. When the user submit the form, the loading image will be displayed until the Ajax call is completed.

Okay, now let's create the page specific .js file (posts_edit.js):

$(function() {

    var _loadingDiv = $("#loadingDiv");

    $('#PostAddForm, #PostEditForm').submit(function(){
        _loadingDiv.show();
        $.post('/posts/ajax_edit',
            $(this).serializeArray(),
            afterValidate,
            "json"
        );
        return false;
    });
 
    function afterValidate(data, status)  {
        $(".message").remove();
        $(".error-message").remove();

        if (data.errors) {
            onError(data.errors);
        } else if (data.success) {
            onSuccess(data.success);
        }
    }
 
    function onSuccess(data) {
        flashMessage(data.message);
        _loadingDiv.hide();
        window.setTimeout(function() {
            window.location.href = '/posts';
        }, 2000);
    };
 
    function onError(data) {
        flashMessage(data.message);
        $.each(data.data, function(model, errors) {
            for (fieldName in this) {
                var element = $("#" + camelize(model + '_' + fieldName));
                var _insert = $(document.createElement('div')).insertAfter(element);
                _insert.addClass('error-message').text(this[fieldName])
            }
            _loadingDiv.hide();
        });
    };
 
    function flashMessage(message) {
        var _insert = $(document.createElement('div')).css('display', 'none');
        _insert.attr('id', 'flashMessage').addClass('message').text(message);
        _insert.insertBefore($(".posts")).fadeIn();
    }

    function camelize(string) {
        var a = string.split('_'), i;
        s = [];
        for (i=0; i<a.length; i++){
            s.push(a[i].charAt(0).toUpperCase() + a[i].substring(1));
        }
        s = s.join('');
        return s;
    }

});

Our jQuery code might be confusing to some of you. Yeah, it's kind of a bit long too. If we see those method names, we'll find out briefly what each method does.

We'll submit the form data to /posts/ajax_edit with jQuery's $.post method, and receive data in JSON format.

Let's take a look at the onError method. What's most important here is that the default DOM ID of each field is different from the name we gave it by default. For example, our “title” field corresponds to the DOM ID “PostTitle” (Camel-cased model and field name). So, we need to modify a little the returned JSON data for error messages to show.

NOTE: You may want to change the URL path to meet your Cake settings. We can't use $this->webroot to point to the web root in a separated .js file, so...for example:

// We can use an absolute URL like this:
$.post('/app/posts/ajax_edit', ...

window.location.href = '/app/posts';

// Or simply use a full URL:
$.post('http://www.example.com/posts/ajax_edit', ...

window.location.href = 'http://www.example.com/posts';

Now let's create a scenario for our controller.

class PostsController extends AppController {

    var $name = 'Posts';
    var $uses = array('Post');
    var $helpers = array('Html', 'Form', 'Javascript');
    var $components = array('RequestHandler');

    function index() {}

    function add() {
        $this->render('edit');
    }
 
    function edit($id = null) {
        if (!$id && empty($this->data)) {
            $this->Session->setFlash(__('Invalid Post', true));
            $this->redirect(array('action'=>'index'));
        }
        if (empty($this->data)) {
            $this->data = $this->Post->read(null, $id);
        }
    } 

    function ajax_edit() {
        Configure::write('debug', 0);
        $this->layout = 'ajax';
        if ($this->RequestHandler->isAjax()) {
            if (!empty($this->data)) {
                $this->Post->create();
                $this->Post->set($this->data['Post']);
                if($this->Post->validates()) {
                    if ($this->Post->save($this->data)) {
                        $message = __('The Post has been saved.', true);
                        $data = $this->data;
                        $this->set('success', compact('message', 'data'));
                    }
                } else {
                    $message = __('The Post could not be saved. Please, try again.', true);
                    $Post = $this->Post->invalidFields();
                    $data = compact('Post');
                    $this->set('errors', compact('message', 'data'));
                }
            }
        }
    }
 
}

The index() is just an empty action which we send the user to after successful form submission, so just skip it. You can also skip the edit and add actions, but notice that we use the same view template (edit.ctp) for both actions just for convenience.

By the way, don't forget to add the RequestHandler component to the $components array, and the Html, Form and Javascript helpers to the $helpers array.

Let's look at our ajax_edit() closely.

We put the actual form elements in our edit (or add) view. Indeed, we post all the data to our Post's controller ajax_edit action.

Technically, there's no big difference with the way we do normally (other than the message handling). The point is that we simply attempt to validate the data, and send all the infomation (no matter what messages we receive after validating or saving the data) to our view.

Now we need some view to return the success/error messages back to jQuery script.

So, let's make the ajax_edit.ctp:

<?php
$output = array();
if($this->validationErrors) {
    $output = Set::insert($output, 'errors', array('message' => $errors['message']));
    $errorMessages = array(
        'Post' => array(
            'title' => array(
                'required' => __("This field cannot be left blank.", true),
                'maxlength' => sprintf(__("Maximum length of %d characters.", true), 30)
            ),
            'body' => array(
                'required' => __("This field cannot be left blank.", true),
                'maxlength' => sprintf(__("Maximum length of %d characters.", true), 200)
            )
        )
    );
    foreach ($errors['data'] as $model => $errs) {
        foreach ($errs as $field => $message) {
            $output['errors']['data'][$model][$field] = $errorMessages[$model][$field][$message];
        }
    }
} elseif ($success) {
    $output = Set::insert($output, 'success', array(
        'message' => $success['message'],
        'data' => $success['data']
    ));
}
echo $javascript->object($output);
?>

What does the output look like? We should get JSON output, something like this:

// Error output
{"errors":{
    "message":"The Post could not be saved. Please, try again.",
    "data":{
        "Post":{
            "title":"This field cannot be left blank.",
            "body":"This field cannot be left blank."
        }
    }
}}

// Success output 
{"success":{
    "message":"The Post has been saved.",
    "data":{
        "Post":{
            "id":"",
            "title":"Lorem ipsum dolor sit amet",
            "body":"Lorem ipsum dolor sit amet, aliquet ...",
            "published":"1"
        }
    }
}}

Alright, let's take another look at a part of our jQuery code:

function onError(data) {
    flashMessage(data.message);
    $.each(data.data, function(model, errors) {
        for (fieldName in this) {
            var element = $("#" + camelize(model + '_' + fieldName));
            var _insert = $(document.createElement('div')).insertAfter(element);
            _insert.addClass('error-message').text(this[fieldName])
        }
        _loadingDiv.hide();
    });
};

If any errors came back from our ajax_edit() action, we create a div element on the fly with the error message using createElement(), a DOM method that creates new document elements.

function flashMessage(message) {
    var _insert = $(document.createElement('div')).css('display', 'none');
    _insert.attr('id', 'flashMessage').addClass('message').text(message);
    _insert.insertBefore($(".posts")).fadeIn();
}

Well, the flashMessage(), which is used by both success and failure, uses the createElement() as well to flash messages just above the form area.

function onSuccess(data) {
    flashMessage(data.message);
    _loadingDiv.hide();
    window.setTimeout(function() {
        window.location.href = '/posts';
    }, 2000);
};

If there is no error, we happily send the user to /posts/index action. We added one gimmick here. We ask the script to wait for 2 seconds before the redirect so that the user can see the success message.

Alright, all done? Did we fulfill the goals we had in the beginning?

There might be some items that are not clear cut:

  • We would like to validate multiple models in a single form view
  • We would like to switch languages based on Localization (l10n)

Here is a tip for the first one. Write a proper code for saving multiple models (we don't go through the multiple save stuff here). We can add as many models as we want by doing something like:

// Our Post's controller ajax_edit action
$Post = $this->Post->invalidFields();
$AnotherModel = $this->AnotherModel->invalidFields();
$data = compact('Post', 'AnotherModel');

And don't forget to add error messages for “AnotherModel” to the $errorMessages array (ajax_edit.ctp)

Okay, let's move to the second one. Since we do view-side validation messages instead of putting error messages into our Post Model, it enables support for multiple languages (l10n).

Phew! All done.

You can download the code of this tutorial at http://code.google.com/p/jamnite/

Thursday, April 16, 2009

Integrate PHP Into JavaScript in CakePHP

Thursday, April 16, 2009 3

The Nuts And Bolts of Cakephp blog recently posted the article “Blend PHP and JavaScript in CakePHP” by Teknoid. He wrote about how to serve JavaScript files with some PHP content by using the $javascript->link() method. That is an interesting approach to PHP/JS integration. After reading his post, I started to consider some alternatives.

Basically, what it comes down to is that you can blend PHP and JavaScript by either the following:

  • Embed JavaScript directly in your view templates (maybe this doesn't mean much to you).
  • Place .js file in your app/vendors/js/ or vendors/js.
  • Use the technique explained in Teknoid's blog.

And here's another alternative, or a more standard way to output files in CakePHP. In this article I am going to explain in detailed steps the way to integrate PHP into JavaScript.

First, parseExtensions needs to be activated (app/config/routes.php).

 Router::parseExtensions('js');

Let’s assume a basic model:

<?php
class User extends AppModel {
    var $name = 'User';
}
?>

Create a table for the User Model with the following SQL:

create table users (
  id int(11) not null auto_increment,
  username varchar(32) not null,
  primary key (id)
);

insert into users (id, username) values (1, 'Jennifer');

Let's build our controller:

<?php
class UsersController extends AppController {
    var $name = 'Users';
    var $uses = array('User');
}
?>

Okay, nothing fancy so far. Let's keep going.

Include the RequestHandler Component in our $components array of either the Users Controller or App Controller:

var $components = array('RequestHandler');

Include the Javascript and Cache Helper in our $helpers array of either the Users Controller or App Controller:

var $helpers = array('Javascript', 'Cache');

As you can see above, yes we are going to use view caching with the Cache Helper. So we'll set 'Cache.check' to true (app/config/core.php).

Configure::write('Cache.check', true);

We'll add an empty action to the Users Controller, for the sake of this demonstration and load some JavaScript file:

function test() {}

Then add another action that renders a JavaScript file:

function alert($id) {
 
    if ($this->params['url']['ext'] != 'js') {
        exit;
    }

    $this->layout = 'gen';
    $this->ext = '.js';
  
    $this->set("cacheDuration", '1 hour');
  
    $data = $this->User->findById($id);
    $this->set('data', $data);
}

In the alert() method, we have changed the file extension from ctp to js so that we can benefit from the code coloring functionality on our text editor.

Now, our Users Controller looks like the following:

<?php
class UsersController extends AppController {

    var $name = 'Users';
    var $uses = array('User');
    var $components = array('RequestHandler');
    var $helpers = array('Javascript', 'Cache');

    function test() {}
 
    function alert($id) {
        if ($this->params['url']['ext'] != 'js') {
            exit;
        }

        $this->layout = 'gen';
        $this->ext = '.js';

        $this->set("cacheDuration", '1 hour');

        $data = $this->User->findById($id);
        $this->set('data', $data);
    }
}
?>

Okay. Let's move on to the view part. We need a view (views/users/test.ctp) to load a JS file:

<?php
if (isset($javascript)) {
    $javascript->link('/users/alert/1', false);
}
?>

The number in the end of the url indicates a user with id=1. So, we are going to retrieve an individual user's data record.

We'll create a layout for JavaScript output (views/layouts/js/gen.ctp):

<cake:nocache><?php header("content-type: application/x-javascript"); ?></cake:nocache><?php
echo $content_for_layout;
if (!$this->cacheAction) {
 $this->cacheAction = $cacheDuration;
}
$this->data = null;
Configure::write('debug', 0);
?>

And a basic view for the alert action (views/users/js/alert.js):

So, if you don't have a js folder in your /app/views/users, create one (this is where you place the alert view template ).

alert("<?php echo $data['User']['username']; ?>");

That's it! Now that we've got a dynamic JavaScript output with view caching, let's quickly open the page located at /users/test in our browser where the alert dialog will appear.

Wednesday, April 8, 2009

Preventing Duplicate Form Submissions in CakePHP

Wednesday, April 8, 2009 0

Forms are a necessary part of web applications and a great way to add data to your database. But somtimes this useful tool may cause problems if your visitors submit the same form information over and over again. How do you solve this with CakePHP or in Cake way? The solution is simple. CakePHP already has one built in, and it's really neat.

Using our Posts controller example, we can make use of Model::postConditions() together with Model::hasAny():

function add() {
 if (!empty($this->data)) {
  $this->Post->set($this->data);
  if($this->Post->validates()) {
   if ($this->Post->hasAny($this->postConditions($this->data))) { 
     $this->Session->setFlash(
      __("Duplicate form submissions are not acceptable.", true)
     );
   } else {
    if ($this->Post->save($this->data)) {
     $this->Session->setFlash(__("Your data has been saved.", true));
    }
   }
  }
 }
}

Hope this is helpful for some of you.

Friday, April 3, 2009

Directly Calling a Model Function from a View

Friday, April 3, 2009 2

It's always better not to call functions in the model from the view, because it breaks somehow the MVC pattern. However, there might be cases where this approach is needed.

A quick example would be:

class Post extends AppModel {

    var $name = 'Post';

    function user($id, $key = null) {
        if (empty($id)) {
            return null;
        }
        $user = ClassRegistry::init('User')->find('first', array(
            'conditions' => array('User.id' => $id),
            'recursive' => -1
        ));
        if (!$user) {
            return null;
        }
        if ($key == null) {
            return $user;
        } else {
           $user = array_pop($user);
           if (isset($user[$key])) {
               return $user[$key];
           }
           return null;
        }
    }

}

See the above example. The method user() is just a sample method that returns an entire User record for a given ID. In addition, if the second argument is given correctly, only a specific field will be retrieved from a returned row.

Now that we have a model function, we can call it in our view:

<? pr( Post::user(1) ); ?>
<? pr( Post::user(1, 'username') ); ?>

Saturday, March 28, 2009

Zodiac Sign Helper Class for CakePHP

Saturday, March 28, 2009 0
This is a simple Helper class for CakePHP that determines what is the zodiacal sign that corresponds to a given date or datetime string.

Releases:

  • Major version released. 1.0.0.0 (New!)

Requirements:

  • CakePHP 1.2 (not tested with CakePHP 1.1.x.x)
  • PHP versions 4 and 5

Licese:

Download:

Installation:

Example Usage:

In your view, just call the ZodiacSignHelper::name(). The passed argument must be a valid date or datetime string. And you will get a Sun zodiac sign such as Virgo, Leo and Sagittarius:

echo $zodiacSign->name($data['User']['birthday']);

You can also get a Chinese zodiac sign by setting the second parameter to 'Chinese'. The Chinese Zodiac consists of a 12-year cycle, each year of which is named after a different animal that imparts distinct characteristics of its year. For example, the year 2009 is the Year of the Ox:

echo $zodiacSign->name($data['User']['birthday'], 'Chinese');

Thursday, March 26, 2009

A More Secure Way to Transfer Session State Between CakePHP Applications

Thursday, March 26, 2009 2

In a previous article entitled "Sharing Session State Across CakePHP Applications", I wrote about the way to transfer session IDs between CakePHP applications. Thanks to the CakePHP development team, CakePHP already has secure session handling; however, some might even think that it is not secure to append a session ID to links. I hear say some search engines indexes URLs with session IDs. Isn't it horrible? One obvious solution would be not to assign them to any of the links on pages.

Let's say we have two sites, siteA.com and siteB.com. We need to maintain a user's session state (authenticated with the Auth Component) when the user jumps from siteA.com to siteB.com by clicking some link.

First, we need to do some settings:

  • Set 'Security.level' to 'low' on siteB.com.
  • Set the session handling method ('Session.save' in app/config/core.php) to 'database'.
  • Use the same Security.salt (/app/config/core.php) for each application.

We are going to use the same session database table for both sites.

Use the CakePHP console to create your session database table:

$ cake schema run create Sessions

Yeah, it's always fun to run the cake console, but you can also use the SQL file found in app/config/sql/sessions.sql.

Create a SiteTransfer Model for each application (on siteA.com and siteB.com):

class SiteTransfer extends AppModel {
    var $name = 'SiteTransfer';
}

Basically the table structure looks something like this:

create table site_transfers (
 id varchar(36) not null,
 sess_id varchar(26) not null,
 primary key (id)
);

In our Users Controller on siteA.com:

class UsersController extends AppController {

  var $name = 'Users';
  
  function index() {}

  function redirectem() {
    $this->autoRender = false;
    App::import('Core', 'String');
    $data['SiteTransfer']['sess_id'] = $this->Session->id();
    $this->SiteTransfer->id = String::uuid();
    if($this->SiteTransfer->save($data)) {
      $this->redirect(
        'http://siteB.com/users/catchem?uuid='.$this->SiteTransfer->id
      );
    }
  }

} 

We have a link saying ‘Go to siteB.com’in the index.ctp view:

echo $html->link('Go to siteB.com', array(
  'action' => 'redirectem'
));

Let's see what's going on here…
When the user on the Index page on siteA.com clicks the link, we redirect the user to the redirectem() action (this action does not need any view). In the 'redirectem' method, we get the current session ID and save it into our site_transfers table with a UUID (i.e. String::uuid). Then we do a redirect to /users/catchem on siteB.com.

Alright, let's build a Users Controller for siteB.com:

class UsersController extends AppController {

  var $name = 'Users';
  var $components = array('Session');

  function beforeFilter() {
    if(!empty($this->params['url']['uuid'])) {
      $uuid = $this->params['url']['uuid'];
      $data = $this->SiteTransfer->findById($uuid);
      $this->Session->id($data['SiteTransfer']['sess_id']);
      $this->SiteTransfer->del($uuid);
    }
  }
  
  function catchem() {
    $this->redirect(array('action' => 'index'));
  }

  function index() {
    pr($this->Session->read('Auth')); 
    exit; 
  }

} 

What happens when the user gets to siteB.com?.
We search our database table (site_transfers) for the UUID token, and then instantiate the session with the session ID from the database. Finally, for security purpose, we need to delete the UUID and session ID from our site_transfers table.

All done! So now the user is logged into both siteA.com and siteB.com.

 
JamNite ◄Design by Pocket, BlogBulk Blogger Templates