Creating a Recent Comments Widget in Agile

Exclusive offer: get 50% off this eBook here
Agile Web Application Development with Yii1.1 and PHP5

Agile Web Application Development with Yii1.1 and PHP5 — Save 50%

Fast-track your Web application development by harnessing the power of the Yii PHP framework

$23.99    $12.00
by Jeffrey Winesett | August 2010 | Open Source PHP Web Development

In this article by Jeffery Winesett, author of Agile Web Application Development with Yii1.1 and PHP5, we are going to turn our focus to the second primary goal of this iteration. As seen in the previous article Adding User Comments in Agile, we now have the ability to leave comments on issues.

We want to display to the user a list of all of the recent comments that have been left on various issues across all of the projects. This will provide a nice snapshot of user communication activity within the application. We also want to build this small block of content in a manner that will allow it to be re-used in various different locations throughout the site. This is very much in the style of web portal applications such as news forums, weather reporting applications and sites such as Yahoo and iGoogle. These small snippets of content are often referred to as portlets, and this is why we referred to building a portlet architecture at the beginning of this iteration. Again, you can refer to http://en.wikipedia.org/wiki/Portlet for more information on this topic.

(For more resources on Agile, see here.)

Introducing CWidget

Lucky for us, Yii is readymade to help us achieve this architecture. Yii provides a component class, called CWidget, which is intended for exactly this purpose. A Yii widget is an instance of this class (or its child class), and is a presentational component typically embedded in a view file to display self-contained, reusable user interface features. We are going to use a Yii widget to build a recent comments portlet and display it on the main project details page so we can see comment activity across all issues related to the project. To demonstrate the ease of re-use, we'll take it one step further and also display a list of project-specific comments on the project details page.

To begin creating our widget, we are going to first add a new public method on our Comment AR model class to return the most recently added comments. As expected, we will begin by writing a test.

But before we write the test method, let's update our comment fixtures data so that we have a couple of comments to use throughout our testing. Create a new file called tbl_comment.php within the protected/tests/fixtures folder. Open that file and add the following content:

<?php

return array(
'comment1'=>array(
'content' => 'Test comment 1 on issue bug number 1',
'issue_id' => 1,
'create_time' => '',
'create_user_id' => 1,
'update_time' => '',
'update_user_id' => '',
),
'comment2'=>array(
'content' => 'Test comment 2 on issue bug number 1',
'issue_id' => 1,
'create_time' => '',
'create_user_id' => 1,
'update_time' => '',
'update_user_id' => '',
),
);

Now we have consistent, predictable, and repeatable comment data to work with.

Create a new unit test file, protected/tests/unit/CommentTest.php and add the following content:

<?php
class CommentTest extends CDbTestCase
{
public $fixtures=array(
'comments'=>'Comment',
);
public function testRecentComments()
{
$recentComments=Comment::findRecentComments();
$this->assertTrue(is_array($recentComments));
}
}

This test will of course fail, as we have not yet added the Comment::findRecentComments() method to the Comment model class. So, let's add that now. We'll go ahead and add the full method we need, rather than adding just enough to get the test to pass. But if you are following along, feel free to move at your own TDD pace. Open Comment.php and add the following public static method:

public static function findRecentComments($limit=10, $projectId=null)
{
if($projectId != null)
{
return self::model()->with(array(
'issue'=>array('condition'=>'project_id='.$projectId)))-
>findAll(array(
'order'=>'t.create_time DESC',
'limit'=>$limit,
));
}
else
{
//get all comments across all projects
return self::model()->with('issue')->findAll(array(
'order'=>'t.create_time DESC',
'limit'=>$limit,
));
}
}

Our new method takes in two optional parameters, one to limit the number of returned comments, the other to specify a specific project ID to which all of the comments should belong. The second parameter will allow us to use our new widget to display all comments for a project on the project details page. So, if the input project id was specified, it restricts the returned results to only those comments associated with the project, otherwise, all comments across all projects are returned.

More on relational AR queries in Yii

The above two relational AR queries are a little new to us. We have not been using many of these options in our previous queries. Previously we have been using the simplest approach to executing relational queries:

  1. Load the AR instance.
  2. Access the relational properties defined in the relations() method.

For example if we wanted to query for all of the issues associated with, say, project id #1, we would execute the following two lines of code:

// retrieve the project whose ID is 1
$project=Project::model()->findByPk(1);

// retrieve the project's issues: a relational query
is actually being performed behind the scenes here
$issues=$project->issues;

This familiar approach uses what is referred to as a Lazy Loading. When we first create the project instance, the query does not return all of the associated issues. It only retrieves the associated issues upon an initial, explicit request for them, that is, when $project->issues is executed. This is referred to as lazy because it waits to load the issues.

This approach is convenient and can also be very efficient, especially in those cases where the associated issues may not be required. However, in other circumstances, this approach can be somewhat inefficient. For example, if we wanted to retrieve the issue information across N projects, then using this lazy approach would involve executing N join queries. Depending on how large N is, this could be very inefficient. In these situations, we have another option. We can use what is called Eager Loading.

The Eager Loading approach retrieves the related AR instances at the same time as the main AR instances are requested. This is accomplished by using the with() method in concert with either the find() or findAll() methods for AR query. Sticking with our project example, we could use Eager Loading to retrieve all issues for all projects by executing the following single line of code:

//retrieve all project AR instances along with their
associated issue AR instances
$projects = Project::model()->with('issues')->findAll();

Now, in this case, every project AR instance in the $projects array already has its associated issues property populated with an array of issues AR instances. This result has been achieved by using just a single join query.

We are using this approach in both of the relational queries executed in our findRecentComments() method. The one we are using to restrict the comments to a specific project is slightly more complex. As you can see, we are specifying a query condition on the eagerly loaded issue property for the comments. Let's look at the following line:

Comment::model()->with(array('issue'=>array('condition'=>'project_
id='.$projectId)))->findAll();

This query specifies a single join between the tbl_comment and the tbl_issue tables. Sticking with project id #1 for this example, the previous relational AR query would basically execute something similar to the following SQL statement:

SELECT tbl_comment.*, tbl_issue.* FROM tbl_comment
LEFT OUTER JOIN tbl_issue ON (tbl_comment.issue_id=tbl_issue.id)
WHERE (tbl_issue.project_id=1)

The added array we specify in the findAll() method simply sets an order by clause and a limit clause to the executed SQL statement.

One last thing to note about the two queries we are using is how the column names that are common to both tables are disambiguated. Obviously when the two tables that are being joined have columns with the same name, we have to make a distinction between the two in our query. In our case, both tables have the create_time column defined. We are trying to order by this column in the tbl_comment table and not the one defined in the issue table. In a relational AR query in Yii, the alias name for the primary table is fixed as t, while the alias name for a relational table, by default, is the same as the corresponding relation name. So, in our two queries, we specify t.create_time to indicate we want to use the primary table's column. If we wanted to instead order by the issue create_time column, we would alter, the second query for example, as such:

return Comment::model()->with('issue')->findAll(array(
'order'=>'issue.create_time DESC',
'limit'=>$limit,
));

Agile Web Application Development with Yii1.1 and PHP5 Fast-track your Web application development by harnessing the power of the Yii PHP framework
Published: August 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

(For more resources on Agile, see here.)

Completing the test

Okay, now that we fully understand what our new method is doing, we need to complete testing of it. In order to fully test our new method, we need to make a few changes to our fixture data. Open each of the fixture data files: tbl_project.php, tbl_issue.php, and tbl_comment.php and ensure each of these entries is in place:

Add the following code in tbl_project:

'project3'=>array(
'name' => 'Test Project 3',
'description' => 'This is test project 3',
'create_time' => ' ',
'create_user_id' => ' ',
'update_time' => ' ',
'update_user_id' => ' ',
),

In tbl_issue, add the following code:

'issueFeature2'=>array(
'name' => 'Test Feature For Project 3',
'description' => 'This is a test feature issue associated with
project # 3 that is completed',
'project_id' => 3,
'type_id' => 1,
'status_id' => 2,
'owner_id' => 1,
'requester_id' => 1,
'create_time' => '',
'create_user_id' => '',
'update_time' => '',
'update_user_id' => '',
),

Finally, add the following code in tbl_comment:

'comment3'=>array(
'content' => 'The first test comment on the first feature issue
associated with Project #3',
'issue_id' => 3,
'create_time' => '',
'create_user_id' => '',
'update_time' => '',
'update_user_id' => '',
),

We now have a total of three comments in our test database. Two of them associated with project #1 and one associated with project #3.

Now we can alter our test method to test:

  • Requesting all comments
  • Limiting the number of returned comments to just two
  • Restricting the returned comments to only those associated with project #3

The following method tests all three scenarios:

public function testRecentComments()
{
//retrieve all the comments for all projects
$recentComments = Comment::findRecentComments();
$this->assertTrue(is_array($recentComments));
$this->assertEquals(count($recentComments),3);

//make sure the limit is working
$recentComments = Comment::findRecentComments(2);
$this->assertTrue(is_array($recentComments));
$this->assertEquals(count($recentComments),2);

//test retrieving comments only for a specific project
$recentComments = Comment::findRecentComments(5, 3);
$this->assertTrue(is_array($recentComments));
$this->assertEquals(count($recentComments),1);
}

We also need to ensure that our CommentTest class is using the fixture data for comments, issues, and projects. Make sure the following fixtures are defined at the top of our CommentTest class:

<?php
class CommentTest extends CDbTestCase
{
public $fixtures=array(
'comments'=>'Comment',
'projects'=>'Project',
'issues'=>'Issue',
);

Now, if we run this test again, we should have all six assertions passing:

>>phpunit unit/CommentTest.php
PHPUnit 3.4.12 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 6 assertions)

Armed with the knowledge of the benefits of Lazy Loading versus Eager Loading in Yii, we should make an adjustment to how the Issue model is loaded within the IssueController::actionView() method. Since we have altered the issues detail view to display our comments, including the author of the comment, we know it will be more efficient to use the Eager Loading approach to load our comments along with their respective authors when we make the call to loadModel() in this method. To do this, we can add a simple input flag to this loadModel() method to indicate whether or not we want to load the comments as well.

Alter the IssueController::loadModel() method as shown below:

public function loadModel($withComments=false)
{
if($this->_model===null)
{
if(isset($_GET['id']))
{
if($withComments)
{
$this->_model=Issue::model()->with(array(
'comments'=>array('with'=>'author')))
->findbyPk($_GET['id']);
}
else
{
$this->_model=Issue::model()->findbyPk($_GET['id']);
}
}
if($this->_model===null)
throw new CHttpException(404,'The requested page does not
exist.');
}
return $this->_model;
}

Now we can change the call to this method in IssueController::actionView(), as such:

public function actionView()
{
$issue=$this->loadModel(true);

With this in place, we will load all of our comments, along with their respective author information, with just one database call.

Creating the widget

Now we are ready to create our new widget to use our new method to display our recent comments.

As we previously mentioned a widget in Yii is a class that extend from the framework class CWidget or one of its child classes. We'll add our new widget to the protected/components/ directly, as the contents of this folder are already specified in the main configuration file to be auto-loaded within the application. This way we on't have to explicitly import the class every time we wish to use it. We'll name our widget RecentComments, so we need to add a php file of the same name to this directly. Add the following class definition to this newly created RecentComment.php file:

<?php
/**
* RecentComments is a Yii widget used to display a list of recent
comments
*/
class RecentComments extends CWidget
{
private $_comments;
public $displayLimit = 5;
public $projectId = null;
public function init()
{
$this->_comments = Comment::model()
->findRecentComments($this->displayLimit,
$this->projectId);
}
public function getRecentComments()
{
return $this->_comments;
}
public function run()
{
// this method is called by CController::endWidget()
$this->render('recentComments');
}
}

The primary work involved when creating a new widget is to override the init() and run() methods of the base class. The init() method initializes the widget and is called after its properties have been initialized. The run() method executes the widget. In this case, we simply initialize the widget by requesting recent comments based on the $displayLimit and $projectId properties. The execution of the widget itself simply renders its associated view file, which we have yet to create. view files, by convention, are placed in views/ directly within the same folder where the widget resides, and have the same name as the widget, but start with a lowercase letter. Sticking with convention, create a new file whose fully qualified path is protected/components/views/renderComments.php. Once created, add the following markup to that file:

<ul>
<?php foreach($this->getRecentComments() as $comment): ?>
<div class="author">
<?php echo $comment->author->username; ?> added a comment.
</div>
<div class="issue">
<?php echo CHtml::link(CHtml::encode($comment->issue->name),
array('issue/view', 'id'=>$comment->issue->id)); ?>
</div>
<?php endforeach; ?>
</ul>

This calls the RenderComments widget's getRecentComments() method, which returns an array of comments. It then iterates over each of them displaying who added the comment and the associated issue on which the comment was left.

In order to see the results, we need to embed this widget into an existing controller view file. As previously mentioned, we want to use this widget on the projects listing page, to display all recent comments across all projects, and also on a specific project details page, to display the recent comments for just that specific project.

Let's start with the project listing page. The view file responsible for displaying that content is protected/views/project/index.php. Open up that file and add the following at the bottom:

<?php $this->widget('RecentComments'); ?>

Now if we view the projects listing page http://localhost/trackstar/index.php?r=project, we see something similar to the following screenshot:

Creating a Recent Comments Widget in Agile

We have now embedded our new recent comments data within the page simply by calling the widget. This is nice, but we can take our little widget one step further to have it display in a consistent manner with all other potential portlets in the application. We can do this by taking advantage of another class provided to us by Yii, CPortlet.

Introducing CPortlet

CPortlet is part of zii, the official extension class library that comes packaged with Yii. It provides a nice base class for all portlet-style widgets. It will allow us to render a nice title as well as consistent HTML markup, so that all portlets across the application can be easily styled in a similar manner. Once we have a widget that renders content (like our RecentComments widget), we can simply use the rendered content of our widget as the content for CPortlet, which itself is a widget, as it also extends from CWidget. We can do this by placing our call to the RecentComments widget between a beginWidget() and an endWiget() call for CPortlet, as such:

<?php $this->beginWidget('zii.widgets.CPortlet', array(
'title'=>'Recent Comments',
));

$this->widget('RecentComments');

$this->endWidget(); ?>

Since CPortlet provides a title property, we set it to be something meaningful for our portlet. We then use the rendered content of the RecentComments widget to drive the content for the porlet widget. The end result of this is depicted in the following screenshot:

Creating a Recent Comments Widget in Agile

This is not a huge change from what we had previously, but we have now placed our content into a consistent container that is already being used throughout the site. Notice the similarity between the right column menu content block and our newly created recent comments content block. I am sure it will come as no surprise to you that this right column menu block is also displayed within a CPortlet container. Taking a peek in protected/views/layouts/column2.php, which is a file that the yiic webapp command autogenerated for us when we initially created the application, reveals the following code:

<?php
$this->beginWidget('zii.widgets.CPortlet', array(
'title'=>'Operations',
));
$this->widget('zii.widgets.CMenu', array(
'items'=>$this->menu,
'htmlOptions'=>array('class'=>'operations'),
));
$this->endWidget();
?>

Adding our widget to another page

Let's also add our portlet to the project details page, and restrict the comments to just those associated with the specific project.

Add the following to the bottom of the protected/views/project/view.php file:

<?php $this->beginWidget('zii.widgets.CPortlet', array(
'title'=>'Recent Project Comments',
));

$this->widget('RecentComments', array('projectId'=>$model->id));

$this->endWidget(); ?>

This is basically the same thing we added to the project listings page, except we are initializing the RecentComments widget's $projectId property by adding an array of name=>value pairs to the call.

Now if we visit a specific project details page, we should see something similar to the following screenshot:

Creating a Recent Comments Widget in Agile

This screenshot shows the details page for project #3, which has one associated issue with just one comment on that issue, as depicted in the picture. You may need to add a few issues and comments on those issues in order to generate a similar display. We now have a way to display recent comments with a few different configurable parameters anywhere throughout the site in a consistent and easily maintainable manner.

Summary

In this article, we were introduced to content components called widgets and portlets. This introduced us to an approach to developing small content blocks and being able to use them anywhere throughout the site. This approach greatly increases reuse, consistency, and ease of maintenance.


Further resources on this subject:


Agile Web Application Development with Yii1.1 and PHP5 Fast-track your Web application development by harnessing the power of the Yii PHP framework
Published: August 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

About the Author :


Jeffrey Winesett

Jeffrey Winesett has over ten years of experience building large-scale, web-based applications. He has been a strong proponent of using open source development frameworks when developing applications, and a champion of the Yii framework in particular since its initial alpha release. He frequently presents on, writes about, and develops with Yii as often as possible.

Books From Packt


PHP Team Development
PHP Team Development

Expert PHP 5 Tools
Expert PHP 5 Tools

Yahoo User Interface 2.X Cookbook: RAW
Yahoo User Interface 2.X Cookbook: RAW

RESTful PHP Web Services
RESTful PHP Web Services

Drupal 7 First look
Drupal 7 First look

MySQL Admin Cookbook
MySQL Admin Cookbook

Magento 1.3: PHP Developer's Guide
Magento 1.3: PHP Developer's Guide

YUI 2.8: Learning the Library
YUI 2.8: Learning the Library


Your rating: None Average: 4 (1 vote)
Recently I have seen this by
Recently I have seen this recent comments column on many blogs and was just wondering how this was possible, I was just thinking that this was because of a module and thought that it is possible only by enabling that module, but now I have got a alternative for that. Thanks for sharing this information…

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
h
r
V
K
p
G
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software