This post covers my notes on Managing Issues (chapter 5) in Yii from the book "Web Application Development with Yii and PHP" by Jeffrey Winesett about learning Yii by taking a step-by-step approach to building a Web-based project task tracking system from conception through production deployment - software development life cycle (SDLC) issue-management application.

  • S c h e m a DB & migrations (atomic)
  • UI for forms
  • Filter - Enforcing a project context
  • Views - get data from other models

The following figure outlines a basic entity-relationship between the users, projects, and issues. Projects can have zero to many users. A user needs to be associated with at least one project but can be associated with many. Issues belong to one and only one project, while projects can have from zero to many issues. Finally an issue is assigned to (or requested by) one single user. s

Run

yiic migrate create create_issue_user_and_assignment_tables

implemented the safeUp() and safeDown() methods:

<?php
 
class m120511_173401_create_issue_user_and_assignment_tables extends CDbMigration
{
    // Use safeUp/safeDown to do migration with transaction
    public function safeUp()
    {
        //create the issue table
        $this->createTable('tbl_issue', array(
            'id' => 'pk',
            'name' => 'string NOT NULL',
            'description' => 'text',
            'project_id' => 'int(11) DEFAULT NULL',
            'type_id' => 'int(11) DEFAULT NULL',
            'status_id' => 'int(11) DEFAULT NULL',
            'owner_id' => 'int(11) DEFAULT NULL',
            'requester_id' => 'int(11) DEFAULT NULL',
            'create_time' => 'datetime DEFAULT NULL',
            'create_user_id' => 'int(11) DEFAULT NULL',
            'update_time' => 'datetime DEFAULT NULL',
            'update_user_id' => 'int(11) DEFAULT NULL',
         ), 'ENGINE=InnoDB');
 
        //create the user table
        $this->createTable('tbl_user', array(
            'id' => 'pk',
            'username' => 'string NOT NULL',
            'email' => 'string NOT NULL',
            'password' => 'string NOT NULL',
            'last_login_time' => 'datetime DEFAULT NULL',
            'create_time' => 'datetime DEFAULT NULL',
            'create_user_id' => 'int(11) DEFAULT NULL',
            'update_time' => 'datetime DEFAULT NULL',
            'update_user_id' => 'int(11) DEFAULT NULL',
         ), 'ENGINE=InnoDB');
 
        //create the assignment table that allows for many-to-many relationship between projects and users
        $this->createTable('tbl_project_user_assignment', array(
            'project_id' => 'int(11) DEFAULT NULL',
            'user_id' => 'int(11) DEFAULT NULL',
            'PRIMARY KEY (`project_id`,`user_id`)',
         ), 'ENGINE=InnoDB');
 
        //foreign key relationships
 
        //the tbl_issue.project_id is a reference to tbl_project.id 
        $this->addForeignKey("fk_issue_project", "tbl_issue", "project_id", "tbl_project", "id", "CASCADE", "RESTRICT");
 
        //the tbl_issue.owner_id is a reference to tbl_user.id 
        $this->addForeignKey("fk_issue_owner", "tbl_issue", "owner_id", "tbl_user", "id", "CASCADE", "RESTRICT");
 
        //the tbl_issue.requester_id is a reference to tbl_user.id 
        $this->addForeignKey("fk_issue_requester", "tbl_issue", "requester_id", "tbl_user", "id", "CASCADE", "RESTRICT");
 
        //the tbl_project_user_assignment.project_id is a reference to tbl_project.id 
        $this->addForeignKey("fk_project_user", "tbl_project_user_assignment", "project_id", "tbl_project", "id", "CASCADE", "RESTRICT");
 
        //the tbl_project_user_assignment.user_id is a reference to tbl_user.id 
        $this->addForeignKey("fk_user_project", "tbl_project_user_assignment", "user_id", "tbl_user", "id", "CASCADE", "RESTRICT");
 
    }
 
    public function safeDown()
    {
        $this->truncateTable('tbl_project_user_assignment');
        $this->truncateTable('tbl_issue');
        $this->truncateTable('tbl_user');
        $this->dropTable('tbl_project_user_assignment');
        $this->dropTable('tbl_issue');
        $this->dropTable('tbl_user');
    }
 
}

Here we have implemented the safeUp() and safeDown() methods rather than the standard up() and down() methods. **Doing this runs these statements in a database transaction with the intent that they are committed or rolled back as a single unit. **

In Issue model changed a few lines:

class Issue extends CActiveRecord
{
    const TYPE_BUG=0;
    const TYPE_FEATURE=1;
    const TYPE_TASK=2;
 
    const STATUS_NOT_STARTED=0;
    const STATUS_STARTED=1;
    const STATUS_FINISHED=2;

Look at the relations:

public function relations()
    {
        // NOTE: you may need to adjust the relation name and the related
        // class name for the relations automatically generated below.
        return array(
            'requester' => array(self::BELONGS_TO, 'User', 'requester_id'),
            'owner' => array(self::BELONGS_TO, 'User', 'owner_id'),
            'project' => array(self::BELONGS_TO, 'Project', 'project_id'),
        );
    }

Adding the issue type drop-down. Normally, gen views look like:

<div class="row">
<?php echo $form->labelEx($model,'type_id'); ?>
<?php echo $form->textField($model,'type_id'); ?>
<?php echo $form->error($model,'type_id'); ?>
</div>
 
//replace the line $form->textField with :
<?php echo $form->dropDownList($model,'type_id', $model-
>getTypeOptions()); ?>

It should be noted that Yii framework base classes make use of the PHP _get "magic" function. This allows us, in our child classes, to write methods such as getTypeOptions() and reference those methods as class properties, using the syntax ->typeOptions. So we could have also used the equivalent syntax when requesting our issue type options array $model->typeOptions.

When uksin drop-down fields, it is good practice to also add a range validation to rules() method to ensure that the submitted value falls within the range of the values allowed by the drop-down. The CRangeValidator attribute, which uses an alias of in, is a good choice to use for defining this validation rule. So we could define such a rule as follows:

array('type_id', 'in', 'range'=>self::getAllowedTypeRange()),
//add a method to return an array of our allowed numerical type values
 
    public function getTypeOptions()
    {
        return array(
            self::TYPE_BUG=>'Bug',
            self::TYPE_FEATURE=>'Feature',
            self::TYPE_TASK=>'Task',
        );
    }

Fixing the owner and requester fields

Another problem we notice with the issue creation form is that the owner and requester fields are also freeform text-input fields. However, we know that these are integer values in the issue table that hold foreign key identifiers to the id column of the tbl_user table. One more problem - need to manage issues within the context of a specific project. That is, a specific project should be chosen before you are able to create a new issue. Currently, application does not enforce this workflow.

Enforcing a project context to ensure that a valid project context is present before we allow access to managing the issues. To do this, we are going to implement what is called a filter. A filter in Yii is a bit of code that is configured to be executed either before or after a controller action is executed. One common example is if we want to ensure that a user is logged in prior to executing a controller action method.

//Defining filters in IssueController.php
    public function filterProjectContext($filterChain)
    {   
        //set the project identifier based on either the GET input 
        //request variables   
        if(isset($_GET['pid']))
            $this->loadProject($_GET['pid']);   
        else
            throw new CHttpException(403,'Must specify a project before performing this action.');
 
        //complete the running of other filters and execute the requested action
        $filterChain->run(); 
    }

We need to add our new filter to this configuration array. To specify that our new filter should be applied to the create action, alter the IssueController::filters() method by adding code

public function filters()
    {
        return array(
            'accessControl', // perform access control for CRUD operations
            'projectContext + create index admin', //check to ensure valid project context
        );
    }

The filters() method should return an array of filter configurations. The previous code returns a configuration that specifies that the projectContext filter, which is defined as a method within the class, should be applied to the actionCreate() method. The configuration syntax allows for the "+" and "-" symbols to be used to specify whether a filter should or should not be applied. If filter to be applied to all the actions except the actionUpdate() and actionView() action methods, we could specify:

return array(
'projectContext - update, view' ,
);

We'll add a project property to the controller class itself. We'll then use a q uerystring parameter in our URLs to indicate the project identifier.

P rivate $ _p r o j e c t = n u l l; //containing the associated Project model instance.
 
    /**
     * Protected method to load the associated Project model class
     * @param integer projectId the primary identifier of the associated Project
     * @return object the Project data model based on the primary key 
     */
    protected f unction loadProject($projectId)  
    {
        if($this->_project===n u l l)
        {
        if($this->_project===n u l l)
        {
            $this->_project=Project::model()->findByPk($projectId);
            if($this->_project===null)
            {
                T h r o w n e w CHttpException(404,'The requested project does not exist.'); 
            }
        }
 
        return $this->_project; 
    }

With this in place, if attempt to create a new issue by clicking on the Create Issue you should see an "Error 403".

Resulted menu:

$this->menu=array(
array('label'=>'List Project', 'url'=>array('index')),
array('label'=>'Create Project', 'url'=>array('create')),
array('label'=>'Update Project', 'url'=>array('update',
'id'=>$model->id)),
array('label'=>'Delete Project', 'url'=>'#', 'linkOptions'=>array('s
ubmit'=>array('delete','id'=>$model->id),'confirm'=>'Are you sure you
want to delete this item?')),
array('label'=>'Manage Project', 'url'=>array('admin')),
array('label'=>'Create Issue', 'url'=>array('issue/create',
'pid'=>$model->id)),
);

So alter the IssueController::actionCreate() method as the following highlighted code suggests:

public function actionCreate()
{
$model=new Issue;
$model->project_id = $this->_project->id;

With these in place, we can easily access all of the issues and/or users associated with a project with incredibly easy syntax. For example:

//instantiate the Project model instance by primary key:
$project = Project::model()->findByPk(1);
//get an array of all associated Issue AR instances
$allProjectIssues = $project->issues;
//get an array of all associated User AR instance
$allUsers = $project->users;
//get the User AR instance representing the owner of
//the first issue associated with this project
$ownerOfFirstIssue = $project->issues[0]->owner;

Open up the view file containing the input form elements /protected/views/issue/_form.php, and find the two text-input field form element definitions for owner_id and requester_id and replace it with the following code:

<?php echo $form->textField($model,'owner_id'); ?>
//with this:
<?php echo $form->dropDownList($model,'owner_id', $model->project-
>getUserOptions()); ?>
//and also replace this line:
<?php echo $form->textField($model,'requester_id'); ?>
//with this:
<?php echo $form->dropDownList($model,'requester_id', $model->project-
>getUserOptions()); ?>

Altering the project controller - actionView() method in the ProjectController class to display a list of the issues associated with a specific project:

public function actionView($id)
    {
        $issueDataProvider=new CActiveDataProvider('Issue', array(
            'criteria'=>array(
                'condition'=>'project_id=:projectId',
                'params'=>array(':projectId'=>$this->loadModel($id)->id),
            ),
            'pagination'=>array(
                'pageSize'=>1,
            ),
         ));
 
        $this->render('view',array(
            'model'=>$this->loadModel($id),
            'issueDataProvider'=>$issueDataProvider,
        ));
 
    }

Altering view.php and add this to the bottom of that file:

<br />
<h1>Project Issues</h1>
<?php $this->widget('zii.widgets.CListView', array(
'dataProvider'=>$issueDataProvider,
'itemView'=>'/issue/_view',
)); ?>
 
// and the last thing - /protected/views/issue/_view.
php file that we specified as a layout template for each issue. Alter the entire contents
of that file to be the following:
<div class="view">
<b><?php echo CHtml::encode($data->getAttributeLabel('name')); ?>:</
b>
<?php echo CHtml::link(CHtml::encode($data->name), array('issue/
view', 'id'=>$data->id)); ?>
<br />
<b><?php echo CHtml::encode($data->getAttributeLabel('descripti
on')); ?>:</b>
<?php echo CHtml::encode($data->description); ?>
<br />
<b><?php echo CHtml::encode($data->getAttributeLabel('type_id'));
?>:</b>
<?php echo CHtml::encode($data->type_id); ?>
<br />
<b><?php echo CHtml::encode($data->getAttributeLabel('status_id'));
?>:</b>
<?php echo CHtml::encode($data->status_id); ?>
</div>

To display the username of the owner and requester (originally in _view "not set" is value) User class instances, change CDetailView configuration to the following (using relations of Issue model access data):

<?php $this->widget('zii.widgets.CDetailView', array(
'data'=>$model,
'attributes'=>array(
'id',
'name',
'description',
array(
'name'=>'type_id',
'value'=>CHtml::encode($model->getTypeText())
),
array(
'name'=>'status_id',
'value'=>CHtml::encode($model->getStatusText())
),
array(
'name'=>'owner_id',
'value'=>isset($model->owner)?CHtml::encode($model->owner-
>username):"unknown"
),
array(
'name'=>'requester_id',
'value'=>isset($model->requester)?CHtml::encode($model-
>requester->username):"unknown" ),
),
)); ?>

With this in place, the associated project will be loaded and available for use. Let's use it in our IssueController::actionIndex() method. Alter that method to be:

public function actionIndex()
{
$dataProvider=new CActiveDataProvider('Issue', array(
'criteria'=>array(
'condition'=>'project_id=:projectId',
'params'=>array(':projectId'=>$this->_project->id),
),
));
$this->render('index',array(
'dataProvider'=>$dataProvider,
));
}

Then we need to add to our criteria in the Issue::search() model class method.

public function search()
{
// Warning: Please modify the following code to remove attributes
that
// should not be searched.
$criteria=new CDbCriteria;
$criteria->compare('id',$this->id);
$criteria->compare('name',$this->name,true);
$criteria->compare('description',$this->description,true);
$criteria->compare('type_id',$this->type_id);
$criteria->compare('status_id',$this->status_id);
$criteria->compare('owner_id',$this->owner_id);
$criteria->compare('requester_id',$this->requester_id);
$criteria->compare('create_time',$this->create_time,true);
$criteria->compare('create_user_id',$this->create_user_id);
$criteria->compare('update_time',$this->update_time,true);
$criteria->compare('update_user_id',$this->update_user_id);
$criteria->condition='project_id=:projectID';
$criteria->params=array(':projectID'=>$this->project_id);
return new CActiveDataProvider(get_class($this), array(
'criteria'=>$criteria,
));
}

Download source code of chapter 5

Leave a Comment

Fields with * are required.

Please enter the letters as they are shown in the image above.
Letters are not case-sensitive.