Sunday, November 22, 2009

Field Name Conflicts - Consider using displayField

displayField
If your table has two fields, i.e., 'name and 'title', you need to specify

     var $displayField = 'name';

in the Model.

Example: 
In my Category table I used both 'name' and 'title' as names of two different fields. Then in my Category model (file:// 'app/models/category.php), I used the following statement to tell Cake to use 'name' as label.

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

The statement var $displayField = 'name' forces Cake to use 'name' as the label. As such, you can choose any field name to use as a label in this way.

CakePHP makes a label from either 'name' or 'title' field. When both are present in your table, the system may get confused. I am not sure if this is the general rule, but, this is how I fixed my problem.

Hope this helps someone.

Tuesday, November 17, 2009

Creating a Complete User Registration System with CakePHP

I was just browsing through my old posts. Hmm. it looks okay. I've started newly with Cake (CakePHP), so, I'm to really learn a lot myself. Anyway, as I'm through my process of learning, I thought it would be great to keep track of what I'm actually reading to get me into the GAME quickly.

My today's task was to learn about a simple user registration system. Once again, I had to go through the CakePHP book and some other references. Instead of listing every detailed step, I would prefer to refer to those MUST read links, which just work like a CHARM in creating a User Management/Registration System using Cake.

Step: 1 Set up CakePHP Console
The console works like a charm. If you had problem in using this console in windows environment, simply, follow the step-by-step method given here.

Step: 2 Follow the CakePHP Simple ACL Control Application
This complete tutorial will guide you through the process of creating your user management system. But before going through this tutorial, please, make sure to understand basic working principles of Cake nicely. The tutorial makes full use of different Cake's core components like Acl Component & Auth Component.

Step: 3 Set custom routing
file: // app/config/routes.php

Copy paste following codes:

Router::connect('/login', array('controller' => 'users', 'action' => 'login'));
Router::connect('/logout', array('controller' => 'users', 'action' => 'logout'));
Router::connect('/register', array('controller' => 'users', 'action' => 'register'));


This will show login form, when someone types http://caketest.local/login and likewise.


Step: 3 Create a dynamic login/logout menu
The Cakebook tutorial does not include creating a dynamic login/logout menu. So, you need to create one.
1. Create a new file.
2. Copy-paste the following code.

<?php 
if(!$session->check('Auth.User')){
echo $html->link('Login','/login');
} else {
$username = $session->read('Auth.User.username');
echo " Hello ". $username ."&nbsp;";
echo $html->link("(logout)", "/logout", array(), null, false);
}
?>

3. Save this file as '/app/views/elements/login_menu.ctp'

4. Open '/app/views/layouts/default.ctp'
5. Copy-paste the following code.

<?php echo $this-> element('login_menu'); ?>

6. Save this file.

Now you can see the login/logout option.
Notice I have used SESSION variables to control login/logout option. To learn more about CakePHP session, please visit this page. For a formatted output of contents inside session variables, use pr($_SESSION) - STRICTLY for DEBUG;


Step 4:  Ban a user account
1.Fire the following SQL query:


ALTER TABLE `users` ADD  `is_banned` TINYINT NOT NULL DEFAULT '0';

This adds a field 'is_banned' in the 'users' table. Set default values to zero.

2. Now copy-paste following code in UsersController::beforeFilter()
file:// app/controllers/users_controller.php

$this->Auth->userScope = array('User.is_banned' => 0);

3. Done. Cake will not allow users to login, when you have set 'is_banned' = 1.
Step 5: Email Validation during user registration
The code is pretty long and nicely explained here. To run with my User model (based on CakePHP's default ACL Component), I needed to make some small adjustment. So, I think it is better to give the codes intact here.

file://  app/controllers/users_controller.php


<?php
 uses('sanitize');
class UsersController extends AppController {



        var $name = 'Users';
var $components = array('Email','Auth');
                                                                    /* "Email' component will handle emailing tasks, 'Auth'    component will handle User Management */
var $helpers = array('Html', 'Form');

/* ..... member functions will go here ... */
}

function beforeFilter()

/* CakePHP CallBack methods */
function beforeFilter() {
   parent::beforeFilter(); 
$this->Email->delivery = 'debug'; /* used to debug email message */
$this->Auth->autoRedirect = false; /* this allows us to run further checks on login() action.*/
$this->Auth->allow('register', 'thanks', 'confirm', 'logout'); 
$this->Auth->userScope = array('User.is_banned' => 0); /* admin can ban a user by updating `is_banned` field of users table to '1' */
}

function register()

// Allows a user to sign up for a new account
        function register() {

                if (!empty($this->data)) {
                        // Applying Auth Components's Password Hashing Rules
/*
We have commented the following field as this was double-hashing password.
$this->Auth->password($this->data['User']['passwrd']); 

*/
                   //      $this->data['User']['passwrd'] = $this->Auth->password($this->data['User']['passwrd']);
 
                        $this->User->data = Sanitize::clean($this->data);
           
// Successfully created account – send activation email     
            
                        if ($this->User->save()) {
                                $this->__sendActivationEmail($this->User->getLastInsertID());


// pr($this->Session->read('Message.email')); /*Uncomment this code to view the content of email FOR DEBUG */


                                // this view is not show / listed – use your imagination and inform
                                // users that an activation email has been sent out to them.
                                $this->redirect('/users/thanks');
                        }
                        // Failed, clear password field
                        else {
                                $this->data['User']['passwrd'] = null;
                        }
                }
$groups = $this->User->Group->find('list');
$this->set(compact('groups'));
        }


Function login()


function login() {
                // Check for incoming login request.
//pr($this->data);
                if ($this->data) {
                        // Use the AuthComponent's login action
                        if ($this->Auth->login($this->data)) {
                                // Retrieve user data
                                $results = $this->User->find(array('User.username' => $this->data['User']['username']), array('User.active'), null, false);
                                // Check to see if the User's account isn't active
                                if ($results['User']['active'] == 0) {
                                        // Uh Oh!
                                        $this->Session->setFlash('Your account has not been activated yet!');
                                        $this->Auth->logout();
                                        $this->redirect('/users/login');
                                }
                                // Cool, user is active, redirect post login
                                else {
                                        $this->redirect('/');
                                }
                        }
                }
        }

function logout()
function logout() {
$this->Session->setFlash('Good-Bye');
$this->redirect($this->Auth->logout());
}


/* function to validate activation link

* and to set 'active' = 1
*/  

function activate()

function activate($user_id = null, $in_hash = null) {

        $this->User->id = $user_id;

if ($this->User->exists() && ($in_hash == $this->User->getActivationHash())) {
         if (empty($this->data)) {

$this->data = $this->User->read(null, $user_id);
   // Update the active flag in the database
$this->User->set('active', 1);
$this->User->save();

$this->Session->setFlash('Your account has been activated, please log in below.');
                $this->redirect('login');
}
}

     // Activation failed, render '/views/user/activate.ctp' which should tell the user.
}


function __sendActivationEmail()

/* function to send activation email */
 function __sendActivationEmail($user_id) {
                $user = $this->User->find(array('User.id' => $user_id), array('User.email', 'User.username','User.id'), null, false);
                if ($user === false) {
                        debug(__METHOD__." failed to retrieve User data for user.id: {$user_id}");
                        return false;
                }

                // Set data for the "view" of the Email
                $this->set('activate_url', 'http://' . env('SERVER_NAME') . '/users/activate/' . $user['User']['id'] . '/' . $this->User->getActivationHash());
                $this->set('username', $this->data['User']['username']);
                
                $this->Email->to = $user['User']['email'];
                $this->Email->subject = env('SERVER_NAME') . ' – Please confirm your email address';
                $this->Email->from = 'noreply@' . env('SERVER_NAME');
                $this->Email->template = 'user_confirm';
                $this->Email->sendAs = 'text';   // you probably want to use both :)    
                return $this->Email->send();

        }

Copy paste function getActivationHash at file:// app/models/user.php

function getActivationHash()
        {
                if (!isset($this->id)) {
                        return false;
                }
                return substr(Security::hash(Configure::read('Security.salt') . $this->field('created') . date('Ymd')), 0, 8);
        }
Copy-paste following code in the file:// app/app_controller.php inside the function beforeFilter()
function beforeFilter() {
$this->Auth->fields = array('username' => 'username', 'password' => 'passwrd');
       /* ... Rest of the function body goes here */
}

Now View Files


Registration form
file:// app/views/users/register.ctp



<h2>Create an Account</h2>
<?php
echo $form->create('User', array('action' => 'register'));
echo $form->input('username');
// Force the FormHelper to render a password field, and change the label.
echo $form->input('group_id', array('type' => 'hidden', 'value' => 'Insert-Default-Value'));
echo $form->input('passwrd', array('type' => 'password', 'label' => 'Password'));
echo $form->input('email', array('between' => 'We need to send you a confirmation email to check you are human.'));
echo $form->submit('Create Account');
echo $form->end();
?>

Notice replace 'Insert-Default-Value' with the actual value of your group_id.
   
Login form
file:// app/views/users/login.ctp



<?php
echo $form->create('User', array('action' => 'login'));
echo $form->input('username');
echo $form->input('passwrd', array('label' => 'Password', 'type' => 'password'));
echo $form->end('Login');
?>



user_confirm.ctp
file:// app/views/elements/email/text/user_confirm.ctp

<?php
  # /app/views/elements/email/text/user_confirm.ctp
  ?>
  Hey there <?= $username ?>, we will have you up and running in no time, but first we just need you to confirm your user account by clicking the link below:
  <?= $activate_url ?>

With all the above scripts, you should be able to get a workable user registration system.
Here, you will have groups/ users/ and you can set group level access per controller, even per action following Cake's default mechanism!

[Acknowledgements]
My sincere regards to Jonny Revees for his wonderful work on this CakePHP user registration system. It works like a charm!


Here are some more stuff I found helpful:
CakePHP Auth Component variables.
Understanding CakePHP Session
Saving data in CakePHP found in book.cakephp.org
Debuggable.com - this post explains how to debug CakePHP email.

Sunday, November 15, 2009

How to List Posts by Categories

There's a lot to bake with Cake. I'm entering into deeper domains. I hope you are also following me.
Today, I'll show you how to list posts by categories. To do so, we will use 'containable' behavior.

Edit Category Model
file:// app/models/category.php
Replace:
var $actsAs = array('Tree');
With
var $actsAs = array('Tree','Containable');
Notice I have added the word 'Containable' in the $actsAs behavior.

Add  listposts() 'action' in Category Controller
file:// app/controllers/categories_controller.php

function listposts() {


$listPosts = $this->Category->find('all');
$this->set(compact('listPosts'));
}
Create a  VIEW file for showing query result.
file:// app/views/categories/listposts.ctp



<h1>Posts found by category</h1>
  <?php
  foreach ($listPosts as $key => $value) {
    
   echo "<h3>" . $value["Category"]["name"] .   "</h3>";
  $posts = $value["Post"];
   foreach ($posts as $post) {
    echo "<b>" . $post["title"] . "</b>";
    echo "<br />";
   }
   }
  ?>


Now point your browser to:
http://caketest.local/categories/listposts


You should see something like this:



That's it!

Saturday, November 14, 2009

How to Join Database Tables (Models) in CakePHP

CakePHP has four association types to handle all database table (model) joining operations.
1. hasOne
2. hasMany
3. belongsTo
4. hasAndBelongsToMany (HABTM)

In our database model, each category should have many posts.
First add a foreign key, category_id.  in the post table.
SQL:


ALTER TABLE `posts` ADD `category_id` INT NOT NULL AFTER `id` ;

Foreign Key Convention
Notice the use of (model_name)_id field in the post table. This is a Cake's convention to name foreign key in related tables. Also notice that I have used category_id, and not categories_id. So, the foreign key rule is to name the foreign key field as (model_name_in_singular)_id in the related table. That's it.

Now I'll update both category model and post model to include their association.

Category Model
File: // app/models/category.php

<?php
class Category extends AppModel {
var $name = 'Category';
var $actsAs = array('Tree');
var $hasMany = 'Post'; /* each category has many posts */
}
?>

Post Model
File: // app/models/post.php

<?php
class Post extends AppModel {
var $name = 'Post';
var $belongsTo = 'Category'; /* each post belongs to a category */
var $validate = array(
'title' => 'notEmpty',
'body'  => 'notEmpty'
);


}
?>

Modify PostsController Class
Now in posts_controller.php, modify your add function.
file: // app/controllers/posts_controller.php


 function add() {
if (!empty($this->data)) {


if ($this->Post->save($this->data)) {
$this->Session->setFlash('Your post has been saved.');
$this->redirect(array('action' => 'index'));
}
} else {


$this->set('categories', $this->Post->Category->generatetreelist(null,null,null," - "));


}


}


Note:
$this->set('categories', $this->Post->Category->generatetreelist(null,null,null," - "));
This will fetch the category tree from categories table.

Modify Related Views to show category tree.
file: // app/views/posts/add.ctp

<h1>Add Post</h1>
<?php
echo $form->create('Post');
echo $form->input('category_id',array('label'=>'Select a category'));
echo $form->input('title');
echo $form->input('body', array('rows' => '3'));
echo $form->end('Save Post');
?>



Now, point your browser to:
http://caketest.local/posts/add

In this example: Cake has created the magical associations to take care of everything. Cake generates Category Tree, displays it in the form, and save category_id in the posts table on the fly.

For a complete tutorial on Models, Saving/ Retrieving Data, and building associations between different models in CakePHP, please see this tutorial.

Now in my next example, I'll try to list posts by categories.

Take care.

Update
Besides, you might just want to update posts as well.

Modify PostsController
Modify edit() function in the following file.
File: // app/controllers/posts_controller.php

function edit($id = null) {
$this->Post->id = $id;
if (empty($this->data)) {
$this->data = $this->Post->read();
$this->set('categories', $this->Post->Category->generatetreelist(null,null,null," - "));


} else {
if ($this->Post->save($this->data)) {
$this->Session->setFlash('Your post has been updated.');
$this->redirect(array('action' => 'index'));
}
}
}

2. Now modify post view
file: // app/views/posts/edit.php


<h1>Edit Post</h1>
<?php
echo $form->create('Post', array('action' => 'edit'));
echo $form->input('category_id',array('label'=>'Select a category'));
echo $form->input('title');
echo $form->input('body', array('rows' => '3'));
echo $form->input('id', array('type'=>'hidden')); 
echo $form->end('Save Post');
?>

Now if you try to edit an existing post, you can see the category tree with the selected category being displayed.
Done.

How to Create a Category Tree with CakePHP 'Tree' behavior

Okay! Let's try to create a category tree using CakePHP (This is something like parent category -> child category type records).

SQL:

CREATE TABLE categories (
id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
parent_id INTEGER(10) DEFAULT NULL,
lft INTEGER(10) DEFAULT NULL,
rght INTEGER(10) DEFAULT NULL,
name VARCHAR(255) DEFAULT '',
PRIMARY KEY  (id)
);

Now insert some record:
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(1, 'Tutorials', NULL, 1, 8);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(2, 'PHP', 1, 2, 5);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(3, 'MySQL', 1, 6, 7);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(4, 'CakePHP', 2, 3, 4);

Now I'll create a model for this category.
1. Create a new file.
2. Copy-paste the following code:

<?php
   class Category extends AppModel {  
var $name = 'Category';
   var $actsAs = array('Tree');
}
?>

3. Save the files as app/models/category.php.

Note:
The variable $actsAs tells Cake to attach 'Tree' behavior to this model, i.e.,  Cake will generate a Tree data structure for category model. I think it is also a good time to introduce you with another fascinating feature of CakePHP - 'Behaviors'. CakePHP has built-in behaviors, like - behaviors for tree structures, translated content, access control list interaction etc., which you can attach with any model. As you might know - 'add', 'edit', 'delete' options for these type of data structures need special care. Cake takes care of it once you have specified the applicable 'behavior' in the model. Behaviors are attached with models using $actsAs variable. In this case, I have specified $actsAs = array('Tree'). This will enforce 'Tree' behavior on Category model. Simple.
To learn more about 'Behaviors', please refer to CakePHP online book.

Now I'll create CategoriesController
Step:
1. Create a new file.
2. Copy-paste the following code.

<?php
class CategoriesController extends AppController {
            var $name = 'Categories';  
             
function index() {
                  $categories = $this->Category->generatetreelist(null, null, null, '&nbsp;&nbsp;&nbsp;');
                  $this->set(compact('categories'));    
                  }
 }
?>
3. Save that file as categories_controller.php under 'app/controllers' folder.

Note: generatetreelist() method generates a tree-type views for our Categories. There are lots of options you can use with this method. For a complete guidelines on options for this method, please refer to CakePHP book.
compact(); function is used to pass variables to your views in CakePHP. Compact() method detects the variables having the same name (in this case 'categories') in your Controller and splits them as an array() of $key => value pairs. Now $this->set() is used to set those values for using them in your view file.

Now I'll create a view for our index() function.
file: '/app/views/categories/index.php'

<?php
echo $html->link("Add Category",array('action'=>'add'));
echo "<ul>";
  foreach($categories as $key=>$value){
  echo "<li>$value</li>";
  }
  echo "</ul>";
?>


Now point your browser to:
http://caketest.local/categories

And you should see following structure:

> Tutorials    
   > PHP    
> CakePHP    
> MySQL

To Add a new category to the list:
1. Open categories_controller.php (found under '/app/controllers')
2. Copy-paste the following function:


function add() {


if (!empty($this -> data) ) {
$this->Category->save($this -> data);
$this->Session->setFlash('A new category has been added');
$this->redirect(array('action' => 'index'));
} else {
$parents[0] = "[Top]";
$categories = $this->Category->generatetreelist(null,null,null," - ");
if($categories) {
foreach ($categories as $key=>$value)
$parents[$key] = $value;
}
$this->set(compact('parents'));
}


}

3. Save this file.

Now we need to create a view file for this add() method (to display the add category form).

1. Create a new file.
2. Copy-paste the following code:


<h1>Add a new category</h1>
<?php
echo $form->create('Category');
echo $form->input('parent_id',array('label'=>'Parent'));
echo $form->input('name',array('label'=>'Name'));
echo $form->end('Add');
?>

3. Save the file as '/app/views/categories/add.ctp'

Now point your browser to this location:
http://caketest.local/categories/add

You should be able to add a new category.

EDIT Category
To edit category, I'll specify a controller action. To do so:
1. Open categories_controller.php (found under '/app/controllers').
2. Copy-paste the following function:


function edit($id=null) {
if (!empty($this->data)) {
if($this->Category->save($this->data)==false)
$this->Session->setFlash('Error saving Node.');
$this->redirect(array('action'=>'index'));
} else {
if($id==null) die("No ID received");
$this->data = $this->Category->read(null, $id);
$parents[0] = "[ Top ]";
$categories = $this->Category->generatetreelist(null,null,null," - ");
if($categories) 
foreach ($categories as $key=>$value)
$parents[$key] = $value;
$this->set(compact('parents'));
}
}


3. Save that function.

Now, I'll write the view file (the form to edit a category). To do so:
1. Create a new file.
2. Copy paste the following code:


<?php
echo $html->link('Back',array('action'=>'index'));
echo $form->create('Category');
  echo $form->hidden('id');
  echo $form->input('name');
  echo $form->input('parent_id', array('selected'=>$this->data['Category']['parent_id']));
  echo $form->end('Update');
?>

3. Now save the file as '/app/views/categories/edit.ctp'.

Hold on!
There is one more thing we should do. We need to show the link to edit record. To do show:
1. Open '/app/views/categories/index.ctp' file.
2. Replace the existing code with this one:

<?php
echo $html->link("Add Category",array('action'=>'add'));
echo "<ul>";
  foreach($categories as $key=>$value){
$edit = $html->link("Edit", array('action'=>'edit', $key));
 echo "<li>$value &nbsp;[$edit]</li>";
  }
  echo "</ul>";
?>

3. Now save the file.

Now point your browser to:
http://caketest.local/categories/
You should be able to see the 'Edit' option against each category name.

Delete Category
CakePHP Format:
removeFromTree($id=null, $delete=false)

Using this method will either delete [to delete, set ($delete=true)] or move a node but retain its sub-tree, which will be re-parented one level higher.

Steps:
1. Open '/app/controllers/categories_controller.php'
2. Copy paste the following code:









function delete($id=null) {
  if($id==null)
  die("No ID received");
  $this->Category->id=$id;
  if($this->Category->removeFromTree($id,true)==false)
     $this->Session->setFlash('The Category could not be deleted.');
   $this->Session->setFlash('Category has been deleted.');
   $this->redirect(array('action'=>'index'));
}

3. Now save that file.

Next, I'll display the option to 'delete' a category.
Step:
1. Open '/app/views/categories/index.ctp
2. Replace the existing code with this one:

<?php
echo $html->link("Add Category",array('action'=>'add'));
echo "<ul>";
  foreach($categories as $key=>$value){
$edit = $html->link("Edit", array('action'=>'edit', $key));
  $delete = $html->link("Delete", array('action'=>'delete', $key));
  echo "<li>$value &nbsp;[$edit]&nbsp;[$delete]</li>";
  }
  echo "</ul>";
?>
3. Save the file.

Done
Point your browser to:
http://caketest.local/categories
Here is a screenshot of what you should see:


[ACKNOWLEDGEMENT]
The code above is mostly based on
Bram Borggreve's beautiful website - Tree Behavior
I express my sincere gratitude to Bram for his wonderful contribution.
Further, to learn more about 'Tree' behavior, please visit: CakePHP Book.





Here is the COMPLETE script files:

1. categories_controller.php ('to be saved under '/app/controllers')


<?php
  class CategoriesController extends AppController {
 var $name = 'Categories';
 function index() {
  $categories = $this->Category->generatetreelist(null, null, null, '&nbsp;&nbsp;&nbsp;');
  // debug ($this->data); die; 
  $this->set(compact('categories')); 
  
  }
  
  function add() {
  
  if (!empty($this -> data) ) {
  $this->Category->save($this -> data);
  $this->Session->setFlash('A new category has been added');
  $this->redirect(array('action' => 'index'));
  } else {
  $parents[0] = "[ Top ]";
  $categories = $this->Category->generatetreelist(null,null,null," - ");
  if($categories) {
  foreach ($categories as $key=>$value)
  $parents[$key] = $value;
  }
  $this->set(compact('parents'));
  }
  
  }

  function edit($id=null) {
  if (!empty($this->data)) {
  if($this->Category->save($this->data)==false)
  $this->Session->setFlash('Error saving Category.');
  $this->redirect(array('action'=>'index'));
  } else {
  if($id==null) die("No ID received");
  $this->data = $this->Category->read(null, $id);
  $parents[0] = "[ Top ]";
  $categories = $this->Category->generatetreelist(null,null,null," - ");
  if($categories) 
  foreach ($categories as $key=>$value)
  $parents[$key] = $value;
  $this->set(compact('parents'));
  }
  }
 function delete($id=null) {
  if($id==null)
  die("No ID received");
  $this->Category->id=$id;
  if($this->Category->removeFromTree($id,true)==false)
  $this->Session->setFlash('The Category could not be deleted.');
  $this->Session->setFlash('Category has been deleted.');
  $this->redirect(array('action'=>'index'));
  }


}
  ?>

2. Category Model (file: '/app/models/category.php')

<?php
class Category extends AppModel { 
var $name = 'Category';
var $actsAs = array('Tree'); 

?> 

3. Views for Category Files
(a)File: '/app/views/categories/index.php'

<?php
echo $html->link("Add Category",array('action'=>'add'));
echo "<ul>";
  foreach($categories as $key=>$value){
$edit = $html->link("Edit", array('action'=>'edit', $key));
  $delete = $html->link("Delete", array('action'=>'delete', $key));
  echo "<li>$value &nbsp;[$edit]&nbsp;[$delete]</li>";
  }
  echo "</ul>";
?>



(b) File: '/app/views/categories/add.php'





<h1>Add a new category</h1>
<?php
echo $form->create('Category');
echo $form->input('parent_id',array('label'=>'Parent'));
echo $form->input('name',array('label'=>'Name'));
echo $form->end('Add');
?>


(c) File: 'app/views/categories/edit.php'

<h1>Add a new category</h1>
<?php
echo $form->create('Category');
echo $form->input('parent_id',array('label'=>'Parent'));
echo $form->input('name',array('label'=>'Name'));
echo $form->end('Add');
?>



Thanks for reading the entry.

Thursday, November 12, 2009

CakePHP - Fine-tune Your Scaffolded Application

In my previous post, I tried to explain how you can create a workable application on the fly using scaffolding. Now I'll try to show you how to fine tune your scaffolded application. Since, I'll be referring to my previous post a lot, so, please read that first.


Control Display Fields

In our previous example, say, I want to display only the comments title, body, and created fields. So, I will add the following lines in comments_controller.php (found under '/app/controllers') just below
   var $scaffold;
statement.


function beforeRender() {
if($this->action === 'index'){
$this->set( 'scaffoldFields', array( 'title', 'body', 'created' ) );
  }
}


Note 'beforeRender()' is a CakePHP callback. It gets executed before the actual controller action. This helps you to add some more logic in your controller. There are three controller callback methods in common use: beforeFilter(), beforeRender(), afterFilter()

And here is that much desired screenshot:


For a more detailed discussion, please go through the book.

How to develop a PHP Application on the FLY Application scaffolding with CakePHP

Application scaffolding is a technique that allows a developer to define and create a basic application that can create, retrieve, update and delete objects.
CakePHP allows application scaffolding on the fly. With this technology we can develop a workable application on the fly.
We have already created 'add', 'edit', 'delete', and 'view' methods for 'posts' table.
Let us create another table, 'comments'.

SQL:


CREATE TABLE `comments` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `title` char(50) NOT NULL,
  `body` text NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY  (`id`)
)

Now create the model file.
Step:
1. Create a new file.
2. Copy-paste following lines:
<?php
class Comment extends AppModel {
var $name = 'Comment';
}
?>
3. Save the file as '/app/models/comment.php

Now create the controller file.
1. Create a new file.
2. Copy-paste following lines:

<?php
class CommentsController extends AppController {

var $name = 'Comments';
var $scaffold;
}
?>

3. Save the file as '/app/controllers/comments_controller.php'

Note:
Please note  $scaffold variable. It tells Cake to create a workable application on the fly.
And that's it.

Now point your browser to:
http://caketest.local/comments

You can see a full featured comments application with 'add', 'view', 'edit', 'delete' option.

Here is a screenshot:
In my next post, I'll try to demonstrate how can you fine-tune your scaffolded application.

Cheers!

CakePHP - Make Your URL SEO Friendly Using Routes

Routing is a feature that maps URLs to controller actions.


URL pattern default routes:
    http://example.com/controller/action/param1/param2/param3

In our example:
http://caketest.local/posts/add   /* here 'posts' is the controller and 'add' is the action.
It will map to: PostsContoller -> add();

http://caketest.local/posts/edit/2 /* here 'posts' is the controller, 'add' is the action, and '2' is the param1
It will map to: PostsController->edit(2);

Routes are defined in '/app/config/routes.php' file using the Router::connect() method.

Basic format:

Router::connect(
    'URL',
    array('paramName' => 'defaultValue'),
    array('paramName' => 'matchingRegex')
)

Now, copy-paste following code in that file:


Router::connect(
'/posts/:slug-:id',
array('controller' => 'posts', 'action' => 'view'),
array(
// order matters
'pass' => array('slug' , 'id'),
'id' => '[0-9]+'
)
);

'/posts/:slug-:id'  is the default URL patter. (Here 'posts' can be replaced by any string, 'slug' is a unique identifier used as url part, and 'id' is the article id).
The next array() will match this URL pattern with $PostsController->view() method.
The array key 'pass' will pass two parameters 'slug' and 'id', the next line sets up rules for acceptable values for 'id'.
That's it.

Now in your index.ctp file, (found under '/app/views/posts/index.ctp')

Replace these lines:
echo $html->link($post['Post']['title'],
"/posts/view/".$post['Post']['id']);

With:
echo $html->link($post['Post']['title'], array(
'controller' => 'posts',
'action' => 'view',
'slug' => Inflector::slug($post['Post']['title'],'-'),
'id' => $post['Post']['id'] )
);

Note: we have used Inflector::slug method to generate the SEO friendly url part.
And you are done. 

To view a particular post,
Add this method at PostsController (File '/app/controllers/posts_controller.php)

function view($id = null, $slug = null) {
$this->Post->id = $this->params['id'];
$this->set('post', $this->Post->read());
}

Now create a new view file: ('/app/views/posts/view.ctp')
<!-- File: /app/views/posts/view.ctp -->
<h1><?php echo $post['Post']['title']?></h1>
<p><small>Created: <?php echo $post['Post']['created']?></small></p>
<p><?php echo $post['Post']['body']?></p>

Now point your browser to:
http://caketest.local/posts/

You can see the list of posts. Note the titles have been made SEO friendly. (Like: http://caketest.local/posts/Second-Blog-Post-Title-2 ). You can read the post by following the post title.

That's it.
Since CakePHP routing has a lot more to do, you MUST read this article thoroughly to understand how it works. This is very important to fine tune your application.

So, I think we have learnt how to include 'ADD', 'UPDATE', 'DELETE', and 'VIEW' operations in a database, plus, how to make SEO friendly urls.

In my next post, I'll tell you how to develop a workable application within minutes using 'scaffolding'.

CakePHP - Edit a Record in Database

So, the keen readers may guess what we need to do to edit a record in our 'posts' table.
Step:
1. Open posts_controller.php (found under '/app/controllers' folder).
2. Copy-paster the function edit() to edit record.


function edit($id = null) 
{   $this->Post->id = $id; 
if (empty($this->data)) { 
$this->data = $this->Post->read(); 
}
else {  
if ($this->Post->save($this->data)) {

  $this->Session->setFlash('Your post has been updated.');
  $this->redirect(array('action' => 'index')); 
}  
}  


}
As such, this function edit() is self-explanatory. If you read it carefully, the function edit() expects '$id' value.
$this=>data contains form submitted data. If it is empty ($this=>data will obviously be empty the first time you try to edit a record), the function $this->Post->read() reads the record from database and stores it at $this->data variable to display the same via edit form. If some data has been submitted, the function $this->Post->save($this->data); tries to save that data by updating the respective record.

Now we need to create a form (Cake calls it 'view') to edit record.
Step:
1. Create a new file.
2. Copy-paste following code:
<h1>Edit Post</h1>
<?php  
echo $form->create('Post', array('action' => 'edit'));  
echo $form->input('title');  
echo $form->input('body', array('rows' => '3'));  
echo $form->input('id', array('type'=>'hidden'));  
echo $form->end('Save Post');  
?>
3. Save this file as 'app/views/posts/edit.ctp'

Now we need to add a link to our index.ctp file to display edit option.

Step:
1. Open index.ctp (found under '/app/views/posts' folder)
2. Add the following line just below the line we have added to show 'delete' option.
<?php echo $html->link('Edit', array('action'=>'edit', 'id'=>$post['Post']['id']));?>
3. Save that file.

Now point your browser to:
http://caketest.local/posts

You can see the 'Edit' option.
Now if you follow 'Edit' link, you can see the form with respective record.
Here is a screenshot:


  
In my next blog post, I'll show you how to make your url SEO friendly using CakePHP.

CakePHP - Delete a Record from Database

If you are following me, you must have learnt how to add a new record to a table.
Let us learn how to delete record.
Steps required:
1. Open file posts_controller.php (Found under '/app/controllers' folder)
2. Add following lines (This is the function to delete record).

function delete($id) {
$this->Post->delete($id);
$this->Session->setFlash('The post with id: '.$id.' has been deleted.');
$this->redirect(array('action'=>'index'));
}
As you can see, $this->Post->delete($id) statement tells Cake (short-form of CakePHP) to delete the record with id = '$id' from posts table.

This is all we need to do in the PostsController (posts_controller.php).

Now let's edit index.ctp file under '/app/views/posts' folder to show 'Delete' option.
Step:
Open the file '/app/views/posts/index.ctp'
Copy the following code and paste it:


<h1>Blog posts</h1>
  <table>
  <tr>
  <th>Id</th>
  <th>Title</th>
  <th>Actions</th>
  <th>Created</th>
  </tr>
 <!-- Here is where we loop through our $posts array, printing out post info -->
<?php foreach ($posts as $post): ?>
  <tr>
  <td><?php echo $post['Post']['id']; ?></td>
  <td>
  <?php echo $html->link($post['Post']['title'], 
  "/posts/view/".$post['Post']['id']); ?>
  </td>
  <td>
  <?php echo $html->link('Delete', array('action' => 'delete', 'id' => $post['Post']['id']), null, 'Are you sure?' )?>
  </td>
  <td><?php echo $post['Post']['created']; ?></td>
  </tr>
  <?php endforeach; ?>
</table>

..............................................................
Save the file. 
Now point your browser to:
http://caketest.local/posts
You can see the screen-shot with DELETE option:


If you follow 'Delete' link, Cake will display a message - 'Are you sure?'
If you confirm delete, the selected record gets deleted.

Now, take a close look at the use of $html->link(); function.

We have learnt already that this function creates a link . This $html->link() function is very versatile like most other functions of CakePHP. 
You can create a link simply by specifying 
$html->link('Link Text','Link Url', array('class'=>'link-class-name'), 'Confirmation Msg [Optional]' );

Here we have created a link in a more sophisticated way. Instead of using link url directly, we have used Cake's powerful link generation feature.
We have used an array() instead of a direct link url.
array('action' => 'delete', 'id' => $post['Post']['id']
Note: we have used 'action'=>'delete' and 'id'=>$post['Post']['id'];
Now Cake will create a link like this one on the fly:
http://caketest.local/posts/delete/2

While going through the CakePHP book I have also noticed that in that array() part, you can actually specify the Controllers also.
array('controller'=>'posts','action' => 'delete', 'id' => $post['Post']['id']) 
Cake will create the same link - http://caketest.local/posts/delete/2 -  without getting confused.
But by default Cake can assume the name of the Controllers in question. So, you may not specify it separately.
That's it.

In my next post, I'll show how to edit record with Cake.

Wednesday, November 11, 2009

How to Set Validation Rules in CakePHP

Setting Validation Rules
I have told you how to add records to a database table using CakePHP here. Now, I'll try to enforce some validation rules on our model (in this case - it is the 'posts' table in our database.).
Steps to be taken:
1. Open 'post.php' file found under 'app/models'.
2. Add the following lines just below var $name = 'Post';


var $validate = array(
'title' => 'notEmpty',
'body'  => 'notEmpty'
);

3. Save the file.
4. And you are done.

Note: We have set one validation rule for each of the two fields. var $validate is a reserved keyword and it tells cake to apply the following validation rules. There are two rules. The first rule applies to 'title' field and the second rule applies to the 'body' field. 'notEmpty' is also a pre-defined validation rule in Cake. So, Cake will not allow you to add records to the 'posts' table, if either 'title' field is empty, or, 'body' field is empty. Simple enough!

Also Note: You need to learn very well how to apply validation rules to develop a robust database driven application.
So, here is the Syntax in Cake( Simple Rules)

var $validate = array('fieldName' => 'ruleName');

Example:
var $validate = array('title' => 'notEmpty');

You can specify as many rules as you want by adding as many 'fieldName => 'ruleName' to that $validate array.
Example:

var $validate = array(
'title' => 'notEmpty',
'body'  => 'notEmpty'
);



To apply validation rules with a custom message, you can use the following Syntax as well. This will help you to write ONE Rule per FIELD.
var $validate = array(
      'fieldName1' => array(
     'rule' => 'ruleName', // or: array('ruleName', 'param1', 'param2' ...)
             'required' => true,
             'allowEmpty' => false,
             'on' => 'create', // or: 'update'
             'message' => 'Your Error Message'
             )
);

To apply MULTIPLE Validation Rules per field, use the following format.

var $validate = array(
     'fieldName' => array(
 
        'ruleName1' => array(
              'rule' => 'ruleName', // extra keys like on, required, etc. go here... ),
          'ruleName2' => array(
'rule' => 'ruleName2', // extra keys like on, required, etc. go here...)
       )
);

Although the structure appears little bit cluttered with use of array(). But with little bit of exercise it does not claim your life to IMPOSE all validation rules to your table fields. At least for me, I could easily add new rules to any field using above format.

Remember:
You need to write validation rules in 'app/models/my_model_name.php file. In our case, we wrote validation rules to 'app/models/post.php file.

Here is a COMPLETE set of Validation Rules applicable for CakePHP.
And here are some more guidelines (really well written) to help you win over this Validation Rule Challenge.

To view this page, point your browser to the following location:

http://caketest.local/posts/add
Here is a screenshot of what happens when I enforce those validation rules:
















Once more, if you have any PLAN to use Cake in future, you need to be a master in framing validation rules. So, just learn this section nicely.


There is something to look at closely 
Note: The url structure of CakePHP
http://your-domain-name.com/controllers/action

In this case, we want to access add() function under PostsController.
So, controller = 'posts' action = 'add'
And the resulting url is:
http://your-domain-name.com/posts/add

In my case, it was:
http://caketest.local/posts/add

I hope it tells how easy it is to access any controllers/action in CakePHP.

Remember:
To hide any function() (Cake calls function as 'action') from direct access, precede it with ('_') underscore.
i.e., _function()   will not be accessible in a web browser.
For example, if you modify our add(); function to look like:
function _add() {
/* Function body goes here */
}
It will not be accessible using
http://caketest.local/posts/_add
Cake will display a message saying :
Private Method in PostsController

This is really useful in case you want to create a function for internal use of your application.
Once again, at least I found this underscore convention pretty simple.


Believe me! Simply follow this non-traditional CakePHP tutorial (If you agree to call it a tutorial at all!). There are lots of stuff under the hood, which will make your coding life really simple!

Take care.