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.

9 comments:

Unknown said...

I like this tutorial as I've been looking for a registration system.

As I'm trying to get this working for myself I have a few questions:
1) is it possible to test everything locally, including the email?
2) does the value SERVER_NAME need to be replaced with your own, or is it referenced from a config file?

Once I submit a registration it doesn't go anywhere and I'm unsure of how to work past this.

Much appreciated.
Paul

Siddhartha said...

1. Yes. It is possible to test everything locally. For email you need to configure your local server to send email.

2. No. It is not required. env('SERVER_NAME') displays the server name, like example.com.

manish said...

this code is give the follwing error .plz help me.thanks

Fatal error: Call to undefined method PagesController::constructClasses() in /opt/lampp/htdocs/cakephp/cake/dispatcher.php on line 206

Siddhartha said...

The code is okay for Cake 1.2, which version of cake are you using?

Anonymous said...

Hi siddhartha its a very well explained article keep it up....i was following ur blog from past 2 weeks as i was new to cakephp but i want to implement a ACL for my product .........it will be nice u post some article how to create an ACL..thanks in advance......once again all ur arctiles regards cakephp very usfull for me really thanks for that.

Siddhartha said...

Thanks Kiran that you liked this blog. Surely, ACL is a great utility. If you have trouble, download and install ACL plugin. You can manage things better.

Unknown said...

Very useful article Siddartha!

Do you have an updated article for Cake 2.x ?

Thanks! and keep up the good work

Unknown said...

i have this problem after Step 3

Notice (8): Undefined variable: session [APP\View\Elements\login_menu.ctp, line 2]


Fatal error: Call to a member function check() on a non-object in D:\xampp\htdocs\mycake\View\Elements\login_menu.ctp on line 2

Please let me know.

Unknown said...

After the Step 3, I am hving this problem
Notice (8): Undefined variable: session [APP\View\Elements\login_menu.ctp, line 2]


Fatal error: Call to a member function check() on a non-object in D:\xampp\htdocs\mycake\View\Elements\login_menu.ctp on line 2

Please let me know.