How Login is handled in Yii Framework


When you browse http:// IP address /index.php?r=site/login in a web application generated by Gii (a Web-based code generator), what will happen? I am interested in the whole process which should help to learn how Yii works.

URL Parsing

When index.php is executed, it will find class Yii to access public method createWebApplication() with $config to get an instance from CWebApplication. Then use this instance to call run() which is defined by CApplication.

Partial source code of index.php:

Yii::createWebApplication($config)->run();

Next, it interprets request in url which are strings after index.php as controller-action. For index.php?r=site/login, site is the controller while login is action. This is explained clearly in CWebApplication:

User requests are resolved as controller-action pairs and additional parameters. CWebApplication creates the requested controller instance and let it to handle the actual user request. If the user does not specify controller ID, it will assume defaultController is requested (which defaults to ‘site’).

Controller class files must reside under the directory controllerPath (defaults to ‘protected/controllers’). The file name and the class name must be the same as the controller ID with the first letter in upper case and appended with ‘Controller’. For example, the controller ‘article’ is defined by the class ‘ArticleController’ which is in the file ‘protected/controllers/ArticleController.php’.

Therefore, it will look for controller SiteController.

Controller: SiteController.php

SiteController is a class declared in /protected/controller/SiteController.php.

It is inherited from Controller which is declared in /protected/components/Controller.php and inherited from class CController.

Partial source code of /protected/controller/SiteController.php:

class SiteController extends Controller

Partial source code of /protected/components/Controller.php:

class Controller extends CController

I didn’t know where to look for action login until I read following explanation in CController. According to this, it will look for public method ActionLogin() for action login.

When a user requests an action ‘XYZ’, CController will do one of the following: 1. Method-based action: call method ‘actionXYZ’ if it exists; 2. Class-based action: create an instance of class ‘XYZ’ if the class is found in the action class map (specified via actions(), and execute the action; 3. Call missingAction(), which by default will raise a 404 HTTP exception.

Source code of public method ActionLogin() in class SiteController:

public function actionLogin()
{
$model=new LoginForm;

// if it is ajax validation request
if(isset($_POST['ajax']) && $_POST['ajax']==='login-form')
{
    echo CActiveForm::validate($model);
    Yii::app()->end();
}

// collect user input data
if(isset($_POST['LoginForm']))
{
    $model->attributes=$_POST['LoginForm'];
    // validate user input and redirect to the previous page if valid
    if($model->validate() && $model->login())
        $this->redirect(Yii::app()->user->returnUrl);
}
// display the login form
$this->render('login',array('model'=>$model));
}

In line 3, it creates an instance $model from class LoginForm which is declared in /protected/models/LoginForm.php.

Line 6 to 19 are skipped because $_POST doesn’t exist.

In line 21, it renders login view with $model. About login view, it means /protected/views/site/login.php. As you may notice, login.php is in the folder which has the same name as the controller (site).

View: login.php

login.php is a file about view in /protected/views/site/login.php.

It is called from controller class SiteController.php and contains code to generate html and JavaScript.

Partial source code of /protected/views/site/login.php:

<?php $form=$this->beginWidget('CActiveForm', array(
    'id'=>'login-form',
    'enableClientValidation'=>true,
    'clientOptions'=>array(
        'validateOnSubmit'=>true,
    ),
)); ?>

CActiveForm is a widget and may provide validation as server-side, AJAX-based, or client-side. According to line 3, it is configured as client-side validation.

Therefore, the rendered page contains a form and JavaScript. It will validate before submitting to back-end using $_POST.

Model: LoginForm.php

LoginForm is a class declared in /protected/models/LoginForm.php.

It is inherited from CFormModel which is inherited from class CModel.

Public method rules() in class LoginForm contains validation rules and is an override public method inherited from class CFormModel defined by class CModel: rules(). Line 9 as below will use public method authenticate() in class LoginForm to check if it hasErrors().

Source code of public method rules() in class LoginForm:

public function rules()
{
        return array(
                // username and password are required
                array('username, password', 'required'),
                // rememberMe needs to be a boolean
                array('rememberMe', 'boolean'),
                // password needs to be authenticated
                array('password', 'authenticate'),
        );
}

Submit Form Data

Once click on submit button, form data will be validated before passing to index.php?r=site/login using $_POST as listed in line 2 below. As we have explained earlier, it means controller site with action login or public method actionLogin() in class SiteController.

Partial source code of rendered view with /protected/views/site/login.php:

<div class="form">
<form id="login-form" action="/index.php?r=site/login" method="post">
    <p class="note">Fields with <span class="required">*</span> are required.</p>

    <div class="row">
        <label for="LoginForm_username" class="required">Username <span class="required">*</span></label>        <input name="LoginForm[username]" id="LoginForm_username" type="text" />        <div class="errorMessage" id="LoginForm_username_em_" style="display:none"></div>    </div>

Now, we have to go through ActionLogin() again. This time $_POST DOES exist now.

Source code of public method ActionLogin() in class SiteController:

public function actionLogin()
{
$model=new LoginForm;

// if it is ajax validation request
if(isset($_POST['ajax']) && $_POST['ajax']==='login-form')
{
    echo CActiveForm::validate($model);
    Yii::app()->end();
}

// collect user input data
if(isset($_POST['LoginForm']))
{
    $model->attributes=$_POST['LoginForm'];
    // validate user input and redirect to the previous page if valid
    if($model->validate() && $model->login())
        $this->redirect(Yii::app()->user->returnUrl);
}
// display the login form
$this->render('login',array('model'=>$model));
}

Line 6 to 10 will be skipped and begin from line 13 because CActiveForm is configured as client-side validation and $_POST[‘ajax’] doesn’t exist.

For line 13, $POST[‘LoginForm’] exist because login.php is rendered with $model in public method ActionLogin() of class SiteController. $model is an instance of class LoginForm as in line 3.

In line 15, public property attributes of class LoginForm is inherited from CFormModel and defined in CModel: attributes.

In line 17, it needs to evaluate if it has been successfully validated and login.

Public method validate() in class LoginForm is inherited from CFormModel and defined in CModel: validate(). It is always true with client-side validation because all validation are executed by client-side JavaScript. If any rule is invalid, client-side JavaScript will display error message next to the field.

Partial source code of rendered view with /protected/views/site/login.php:

        <label for="LoginForm_username" class="required">Username <span class="required">*</span></label>
        <input name="LoginForm[username]" id="LoginForm_username" type="text" />
        <div class="errorMessage" id="LoginForm_username_em_" style="display:none"></div>
jQuery(function($) {
jQuery('#login-form').yiiactiveform({'validateOnSubmit':true,'attributes':[{'id':'LoginForm_username','inputID':'LoginForm_username','errorID':'LoginForm_username_em_','model':'LoginForm','name':'username','enableAjaxValidation':false,'clientValidation':function(value, messages, attribute) {

if(jQuery.trim(value)=='') {
    messages.push("Username cannot be blank.");
}

Because $_identity is null in public method login() in class LoginForm, it creates an instance of class UserIdentity which is in /protected/components/UserIdentity.php with username and password in line 5. Then use public method authenticate() in class UserIdentity to compare in line 6.

Source code of public method login() in class LoginForm:

public function login()
{
    if($this->_identity===null)
    {
        $this->_identity=new UserIdentity($this->username,$this->password);
        $this->_identity->authenticate();
    }
    if($this->_identity->errorCode===UserIdentity::ERROR_NONE)
    {
        $duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days
        Yii::app()->user->login($this->_identity,$duration);
        return true;
    }
    else
        return false;
}

If invalid, errorCode will be returned and is not ERROR_NONE.

Source code of public method authenticate() in class UserIdentity:

public function authenticate()
{
    $users=array(
        // username => password
        'demo'=>'demo',
        'admin'=>'turnkey',
    );
    if(!isset($users[$this->username]))
        $this->errorCode=self::ERROR_USERNAME_INVALID;
    elseif($users[$this->username]!==$this->password)
        $this->errorCode=self::ERROR_PASSWORD_INVALID;
    else
        $this->errorCode=self::ERROR_NONE;
    return !$this->errorCode;
}

Now, back to public method actionLogin() in class SiteController. If both validate() and login() are true, page will be redirect to previous URL. Or else will render login.php with $model again.

Once login is invalid, the ‘authenticate’ in public method rules() in class LoginForm will trigger hasErrors() using public method authenticate() in class LoginForm. Error message will be added by addError() as in line 7 below. These messages will show up when rendering in public method ActionLogin() of class SiteController:

Source code of public method authenticate() in class LoginForm:

public function authenticate($attribute,$params)
{
    if(!$this->hasErrors())
    {
        $this->_identity=new UserIdentity($this->username,$this->password);
        if(!$this->_identity->authenticate())
            $this->addError('password','Incorrect username or password.');
    }
}

Thoughts

It takes me a lot of time to trace the program and helps to understand how Yii handles URL and apply MVC architecture. You will need to check following source code file and Class Reference very often.

  1. /protected/controller/SiteController.php
  2. /protected/models/LoginForm.php.
  3. /protected/views/site/login.php
  4. /protected/components/Controller.php
  5. /protected/components/UserIdentity.php

Here is a simple hierarchy relationship among mentioned class:

CApplication ← CWebApplication
CController ← Controller ← SiteController
CModel ← CFormModel ← LoginForm
CUserIdentity ← UserIdentity

Reference

  1. Wiki: IP address
  2. Wiki: Model–view–controller
  3. Wiki: Uniform resource locator
  4. Yii Framework
  5. Yii Framework: Documentation: Class Reference
  6. Yii Framework: Documentation: CActiveForm
  7. Yii Framework: Documentation: CApplication
  8. Yii Framework: Documentation: CApplication: run()
  9. Yii Framework: Documentation: CController
  10. Yii Framework: Documentation: CFormModel
  11. Yii Framework: Documentation: CModel
  12. Yii Framework: Documentation: CModel: attributes
  13. Yii Framework: Documentation: CModel: addError()
  14. Yii Framework: Documentation: CModel: hasErrors()
  15. Yii Framework: Documentation: CModel: rules()
  16. Yii Framework: Documentation: CModel: validate()
  17. Yii Framework: Documentation: CWebApplication
  18. Yii Framework: Documentation: Yii
  19. Yii Framework: Documentation: Yii: createWebApplication()
  20. Yii Framework: Forum: Yii Autentication With Mysql
  21. Yii Framework: Tutorials: Automatic Code Generation

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.