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.

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
{
public function safeUp()
{
$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');
$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');
$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');
$this->addForeignKey("fk_issue_project", "tbl_issue", "project_id", "tbl_project", "id", "CASCADE", "RESTRICT");
$this->addForeignKey("fk_issue_owner", "tbl_issue", "owner_id", "tbl_user", "id", "CASCADE", "RESTRICT");
$this->addForeignKey("fk_issue_requester", "tbl_issue", "requester_id", "tbl_user", "id", "CASCADE", "RESTRICT");
$this->addForeignKey("fk_project_user", "tbl_project_user_assignment", "project_id", "tbl_project", "id", "CASCADE", "RESTRICT");
$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:
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>
<?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()),
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.
public function filterProjectContext($filterChain)
{
if(isset($_GET['pid']))
$this->loadProject($_GET['pid']);
else
throw new CHttpException(403,'Must specify a project before performing this 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',
'projectContext + create index admin',
);
}
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;
@param @return
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:
$project = Project::model()->findByPk(1);
$allProjectIssues = $project->issues;
$allUsers = $project->users;
$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'); ?>
<?php echo $form->dropDownList($model,'owner_id', $model->project-
>getUserOptions()); ?>
<?php echo $form->textField($model,'requester_id'); ?>
<?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',
)); ?>
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()
{
that
$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