6 min read

(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,
));

LEAVE A REPLY

Please enter your comment!
Please enter your name here