(For more resources on Agile, see here.)
The goal of this iteration is to implement feature functionality in the Trackstar application to allow users to leave and read comments on issues. When a user is viewing the details of any project issue, they should be able to read all comments previously added as well as create a new comment on the issue. We also want to add a small fragment of content, or portlet, to the project-listing page that displays a list of recent comments left on all of the issues. This will be a nice way to provide a window into recent user activity and allow easy access to the latest issues that have active conversations.
The following is a list of high-level tasks that we will need to complete in order to achieve these goals:
- Design and create a new database table to support comments
- Create the Yii AR class associated with our new comments table
- Add a form directly to the issue details page to allow users to submit comments
- Display a list of all comments associated with an issue directly on the issues details page
Creating the model
As always, we should run our existing test suite at the start of our iteration to ensure all of our previously written tests are still passing as expected. By this time, you should be familiar with how to do that, so we will leave it to the reader to ensure that all the unit tests are passing before proceeding.
We first need to create a new table to house our comments. Following is the basic DDL definition for the table that we will be using:
CREATE TABLE tbl_comment
`id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
`content` TEXT NOT NULL,
As each comment belongs to a specific issue, identified by the issue_id, and is written by a specific user, indicated by the create_user_id identifier, we also need to define the following foreign key relationships:
ALTER TABLE `tbl_comment` ADD CONSTRAINT `FK_comment_issue`
FOREIGN KEY (`issue_id`) REFERENCES `tbl_issue` (`id`);
ALTER TABLE `tbl_comment` ADD CONSTRAINT `FK_comment_author`
FOREIGN KEY (`create_user_id`) REFERENCES `tbl_user` (`id`);
If you are following along, please ensure this table is created in both the trackstar_dev and trackstar_test databases.
Once a database table is in place, creating the associated AR class is a snap. We simply use the Gii code creation tool's Model Generator command and create an AR class called Comment.
Since we have already created the model class for issues, we will need to explicitly add the relations to to the Issue model class for comments. We will also add a relationship as a statistical query to easily retrieve the number of comments associated with a given issue (just as we did in the Project AR class for issues). Alter the Issue::relations() method as such:
public function relations()
'requester' => array(self::BELONGS_TO, 'User', 'requester_id'),
'owner' => array(self::BELONGS_TO, 'User', 'owner_id'),
'project' => array(self::BELONGS_TO, 'Project', 'project_id'),
'comments' => array(self::HAS_MANY, 'Comment', 'issue_id'),
'commentCount' => array(self::STAT, 'Comment', 'issue_id'),
Also, we need to change our newly created Comment AR class to extend our custom TrackStarActiveRecord base class, so that it benefits from the logic we placed in the beforeValidate() method. Simply alter the beginning of the class definition as such:
* This is the model class for table "tbl_comment".
class Comment extends TrackStarActiveRecord
We'll make one last small change to the definitions in the Comment::relations() method. The relational attributes were named for us when the class was created. Let's change the one named createUser to be author, as this related user does represent the author of the comment. This is just a semantic change, but will help to make our code easier to read and understand. Change the method as such:
* @return array relational rules.
public function relations()
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
'author' => array(self::BELONGS_TO, 'User', 'create_user_id'),
'issue' => array(self::BELONGS_TO, 'Issue', 'issue_id'),
Creating the Comment CRUD
Once we have an AR class in place, creating the CRUD scaffolding for managing the related entity is equally as easy. Again, use the Gii code generation tool's Crud Generator command with the AR class name, Comment, as the argument. Although we will not immediately implement full CRUD operations for our comments, it is nice to have the scaffolding for the other operations in place.
As long as we are logged in, we should now be able to view the autogenerated comment submission form via the following URL:
(For more resources on Agile, see here.)
Altering the scaffolding to meet requirements
As we have seen many times before, we often have to make adjustments to the autogenerated scaffolding code in order to meet the specific requirements of the application. For one, our autogenerated form for creating a new comment has an input field for every single column defined in the tbl_comment database table.
We don't actually want all of these fields to be part of the form. In fact, we want to greatly simplify this form to have only a single input field for the comment content. What's more, we don't want the user to access the form via the above URL, but rather only by visiting an issue details page. The user will add comments on the same page where they are viewing the details of the issue. We want to build towards something similar to what is depicted in the following screenshot:
In order to achieve this, we are going to alter our Issue controller class to handle the post of the comment form as well as alter the issue details view to display the existing comments and new comment creation form. Also, as comments should only be created within the context of an issue, we'll add a new method to the Issue model class to create new comments.
Adding a comment
Let's start by writing a test for this new public method on the Issue model class. Open up the IssueTest.phpfile and add the following test method:
public function testAddComment()
$comment = new Comment;
$comment->content = "this is a test comment";
This, of course, will fail until we add the method to our Issue AR class. Add the following method to the Issue AR class:
* Adds a comment to this issue
public function addComment($comment)
This method ensures the proper setting of the comment issue ID before saving the new comment. Run the test again to ensure it now passes.
With this method in place, we can now turn focus to the issue controller class. As we want the comment creation form to display from and post its data back to the IssueController::actionView() method, we will need to alter that method. We will also add a new protected method to handle the form POST request. First, alter the actionView() method to be the following:
public function actionView()
Then add the following protected method to create a new comment and handle the form post request for creating a new comment for this issue
protected function createComment($issue)
has been added." );
Our new protected method, createComment() is responsible for handling the POST request for creating a new comment based on the user input. If the comment is successfully created, the page will be refreshed displaying the newly created comment. The changes made to IssueController::actionView() are responsible for calling this new method and also feeding the new comment instance to the view.
Displaying the form
Now we need to alter our view. First we are going to create a new view file to render the display of our comments and the comment input form. As we'll render this as a partial view, we'll stick with the naming conventions and begin the filename with a leading underscore. Create a new file called _comments.php under the protected/views/issue/ folder and add the following code to that file:
<?php foreach($comments as $comment): ?>
<?php echo $comment->author->username; ?>:
on <?php echo date('F j, Y \a\t h:i a',strtotime($comment->create_time)); ?>
<?php echo nl2br(CHtml::encode($comment->content)); ?>
</div><!-- comment -->
<?php endforeach; ?>
This file expects as an input parameter an array of comment instances and displays them one by one. We now need to alter the view file for the issue detail to use this new file. We do this by opening protected/views/issue/view.php and adding the following to the end of the file:
<?php if($model->commentCount>=1): ?>
<?php echo $model->commentCount>1 ? $model->commentCount . '
comments' : 'One comment'; ?>
<?php endif; ?>
<h3>Leave a Comment</h3>
<?php if(Yii::app()->user->hasFlash('commentSubmitted')): ?>
<?php echo Yii::app()->user->getFlash('commentSubmitted'); ?>
<?php else: ?>
<?php endif; ?>
Here we are taking advantage of the statistical query property, commentCount, we added earlier to our Issue AR model class. This allows us to quickly determine if there are any comments available for the specific issue. If there are comments, it proceeds to render them using our _comments.php display view file. It then displays the input form that was created for us when we used the Gii Crud Generator functionality. It will also display the simple flash message set upon a successfully saved comment.
One last change we need to make is to the comment input form itself. As we have seen many times in the past, the form created for us has an input field for every column defined in the underlying tbl_comment table. This is not what we want to display to the user. We want to make this a simple input form where the user only needs to submit the comment content. So, open up the view file that houses the input form, that is, protected/views/comment/_form.php and edit it to be simply:
<?php $form=$this->beginWidget('CActiveForm', array(
<p class="note">Fields with <span class="required">*</span>
<?php echo $form->errorSummary($model); ?>
<?php echo $form->labelEx($model,'content'); ?>
<?php echo $form->textArea($model,'content',array('rows'=>6,
<?php echo $form->error($model,'content'); ?>
<div class="row buttons">
<?php echo CHtml::submitButton($model->isNewRecord ?
'Create' : 'Save'); ?>
<?php $this->endWidget(); ?>
With all of this in place, we can visit an issue listing page, for example http://hostname/trackstar/index.php?r=issue/view&id=1
And we see the following comment input form at the bottom of the page:
If we attempt to submit the comment without specifying any content, we see an error as depicted in the following screenshot:
And then, if we are logged in as Test User One and we submit the comment My first test comment, we are presented with the following display:
With this iteration, we have started to flesh out our Trackstar application with functionality that has come to be expected of most user-based web applications today. The ability for users to communicate with each other within the application is an essential part of a successful issue management system.
As we created this essential feature, we were able to deeper look into how to write relational AR queries. In the next article Creating a Recent Comments Widget in Agile, we will learn to create Recent Comments widgets.
- Creating a Recent Comments Widget in Agile [Article]
- Agile with Yii 1.1 and PHP5: The TrackStar Application [Article]
- Agile Works Best in PHP Projects [Article]