应用Yii1.1和PHP5进行敏捷Web开发10

第九章:迭代6:添加用户评论

在上两次迭代方法中,随着用户管理可执行,我们Trackstar才真正变得有条理。现在,我们将应用程序的主要功能实现抛到身后。我们可以开始关注一些nice-to-have(更好)的功能。首先,我们要做的就是开发一个让用户可以给主题作出评论的权限。

提供一个主题跟踪工具是用户评论功能的一个重要部分,其中一个方式是让用户直接留言,内容来自一个主题的对话。该内容将成为这个主题的一个即时会话,这也有助于时刻了解所有主题从发起到结束的整个过程。为了让用户了解这些内容,我们也将使用更多内容来说明使用Yii挂件和建立一个组件模型(要了解更多关于 Portlets的信息,请参考http://en.wikipedia.org/wiki/Portlet)。

迭代计划

这一节的目标是在Trackstar程序里实现允许用户阅读主题和发表看法的功能,当用户查看任何项目的细节时,他们应该能够读取以前添加的所有评论,以及关于这个主题上新的评论。我们也希望能够给项目列表页面增加一些主题内容剪辑或者portlet,以便在所有主题的左侧显示一个最新内容的列表。对于了解最近用户的活跃性,并允许用户方便地访问到正在积极讨论的最新主题,提供这样一个区域将是一个不错的方式。

我们将要一步一步完成下面这些高级任务来实现上述目标:

    • 设计并创建一个支持我们工作的数据库;
    • 创建一个Yii的AR类关联到我们创建的数据库表;
    • 在主题详情页面添加一个表单以供用户提交评论;
    • 在主题详情页面显示所有与主题相关联的内容列表;
    • 在项目列表页面利用Yii挂件的列出显示大部分最近内容。

添加模型

跟往常一样,我们可以运行已有的测试程序,我们事先写好的程序达到了我们的预期目标。这时,你应该熟悉了如何做到的。我们把已经完成的源码留给读者,已确保所有的单元都能正常运行。

首先我们要创建一个数据库表,下面是这个表的内容:

CREATE TABLE tbl_comment 
(
    `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, 
    `content` TEXT NOT NULL, 
    `issue_id` INTEGER, 
    `create_time` DATETIME,
    `create_user_id` INTEGER, 
    `update_time` DATETIME, 
    `update_user_id` INTEGER
)

由于每个评论属于一个具体问题,由issue_id确定,并由create_user_id标识的特定用户编写,另外我们还需要定义以下外键关系:

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`);

如果你是依照步骤来的,请确保该表在trackstar_dev和trackstar_test两个数据库里均已创建。

一旦一个数据库表就位后,添加AR关联类就变得易如反掌了。在先前的章节里我们看过许多次了,我们知道如何正确的实现它。我们简单地使用Gii代码创建工具里的“Model Generator”命令添加一个叫Comment的AR类。如果你不明白,可以返回到第5和第6章查阅如何使用Gii创建模型类。

既然我们已经创建了问题(issue)模型类,我们需要在Issue模型类中添加一条关联到评论(comment)模型。我们还要添加一条关联用于统计查询对应问题(issue)的相关评论数量(正是我们在Project AR类中对问题(issues)所做的一样)。Issuse::relations()方法修改如下:

public function relations() 
{
    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'), 
        'comments' => array(self::HAS_MANY, 'Comment', 'issue_id'), 
        'commentCount' => array(self::STAT, 'Comment', 'issue_id'),
    );
}

同样的,我们需要改变我们新近创建的Comment AR类来扩展我们的定制TrackStarActiveRecord基类,以便它通过我们命名为beforeValidate()的方法中获益。只需改变类的定义的头部,就像下面这样的:

<?php
/** 
   * This is the model class for table "tbl_comment". 
   */
class Comment extends TrackStarActiveRecord
{

我们将在Comment::relations()方法里做最后一次小的变动。关系属性被创建时以作者的用户名命名,我们将createUser更名为author,这个相关的用户不代表评论作者。这只是一种演变,但是将有助于我们的代码更容易阅读和理解。修改方法如下:

/** 
   * @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. 
    return array(
        'author' => array(self::BELONGS_TO, 'User', 'create_user_id'),
        'issue' => array(self::BELONGS_TO, 'Issue', 'issue_id'), 
    );
}

创建一个评论的CRUD

一旦我们的AR类就位,创建一个用于管理实体CRUD脚手架同样容易。同样的,使用Gii工具的Crud Generator 命令创建一个名字为Comment的AR类。同样的,我们在以前的章节中看到很多次了,所以我们将这个过程留给读者作为练习。同样,如果有需要,可以回到第5章和第6章如何使用Gii创建CRUD代码。尽管不能让CRUD马上为我们操作评论,但是在某些地方有这样一个脚手架还是相当好使的。

只要我们都已经登录了,我们现在应该能够查看自动生成的评论,通过以下址址访问:http://localhost/trackstar/index.php?r=comment/create

修改脚手架以满足要求

就像之前我们看过的许多次一样,我们不得不对脚手架自动生成的代码进行修改以符合应用程序所要达到的要求,比如,我们的自动生成表单对于数据库里的tbl_comment表内定义的每一个字段都自动生成了一个用于输入的输入框。

我们实际并不需要所有的字段都生成表单。事实上,我们只需要填写评论的这一个输入框就够了。更何况,我们不希望用户通过输入上述网址来访问,而是通过访问一个问题的详细页面。用户将在问题的详情页面添加评论。我们要建立的页面类似下面的截图。

images/book1/chapter9/1.jpg

为了达到这一目标,我们将修改我们的Issue控制器以处理发布评论的方式,同时修改详情视图,用来显示已有的评论和创建新评论的表单。此外,一个评论应该出现在问题的背景范围之内,我们将给Issue模型添加一个新的方法用来创建新的评论。

添加新评论

我们来给Issue模型里的public方法写一个新的测试,打开IssueTest.php 添加下列代码:

public function testAddComment() 
{
    $comment = new Comment; 
    $comment->content = "this is a test comment"; 
    $this->assertTrue($this->issues('issueBug')->addComment($comment));
}

当然,这些还不算完工,我们还需要给Issue的AR类添加一个新方法,把下列代码加入Issue的AR类:

/** 
   * Adds a comment to this issue 
   */
public function addComment($comment) 
{
   $comment->issue_id=$this->id; 
   return $comment->save();
}

此方法可以确保在保存问题的评论之前ID已经被正确设置。再次运行测试,以确保它现在可以通过。

一旦我们的方式完成,我们可以把注意力集中到issue的控制器类上,随着我们希望发表新的评论,以把显示其数据回 IssueController::actionView()方法,我们将需要修改该方法。我们还是添加一个受保护的方法来处理表单里POST请求。首先,修改actionView() ,如下:

public function actionView() 
{
    $issue=$this->loadModel(); 
    $comment=$this->createComment($issue);
    $this->render('view',array( 
        'model'=>$issue, 
        'comment'=>$comment,
    ));
}

然后添加下列protected方法用来创建一个新的评论和处理这个新评论表单的post请求:

protected function createComment($issue) 
{
    $comment=new Comment; 
    if(isset($_POST['Comment'])) 
    {
        $comment->attributes=$_POST['Comment']; 
        if($issue->addComment($comment)) 
        {
                Yii::app()->user->setFlash('commentSubmitted',"Your comment has been added." );
                $this->refresh();
        } 
    }
    return $comment;
}

新的受保护的方法createComment() 是负责处理用户输入的一个新评论的POST请求。如果这个评论成功创建,该页面将刷新以显示新的评论。 IssueController::actionView() 所作的更改负责新方法的调用,同时也给这个评论的例子填充显示的数据。

显示表单

现在,我们需要修改视图。首先我们要创建一个新的视图文件来渲染我们的评论和评论的输入表单,我们将延续命名规定,开始在文件名前加一个下划线。在 protected/views/issue/ 下创建一个新文件命名为_comments.php,并在该文件内输入下列代码:

<?php foreach($comments as $comment): ?> 
<div class="comment">
    <div class="author"> 
        <?php echo $comment->author->username; ?>:
    </div>
 
    <div class="time"> 
        on <?php echo date('F j, Y \a\t h:i a',strtotime($comment->create_time)); ?> 
    </div>
 
    <div class="content">
        <?php echo nl2br(CHtml::encode($comment->content)); ?> 
    </div>
    <hr> 
</div>
<!-- comment --> 
<?php endforeach; ?>

该文件将成为输入参数数组的一个实例,并将这些参数一个一个显示出来。我们现在需要为问题详情来修改view文件。我们这么做,打开protected/views/issue/view.php,在文件末尾添加下列代码:

<div id="comments"> 
    <?php if($model->commentCount>=1): ?>
        <h3> 
            <?php echo $model->commentCount>1 ? $model->commentCount . 'comments' : 'One comment'; ?> 
        </h3>
 
        <?php $this->renderPartial('_comments',array( 'comments'=>$model->comments,)); ?> 
    <?php endif; ?>
 
    <h3>Leave a Comment</h3>
 
    <?php if(Yii::app()->user->hasFlash('commentSubmitted')): ?> 
    <div class="flash-success">
        <?php echo Yii::app()->user->getFlash('commentSubmitted'); ?> 
    </div>
    <?php else: ?> 
        <?php $this->renderPartial('/comment/_form',array('model'=>$comment, )); ?>
    <?php endif; ?> 
</div>

这里我们利用统计查询特性commentCount来增强先前的Issue AR模型类。这让我们对出现的任何特定问题都能快速的作出判断。如果是评论,它将使用_comments.php 视图文件来渲染它们,它将显示我们用Gii Crud Generator创建的表单。它也会显示一个提示保存成功的一闪而过信息。

我们要做的最后一个修改是评论表单本身。就像我们在前面看到的,表单为tbl_comment 数据表里的每个字段都创建了一个输入框,这不是我们想要给用户的。我们需要的是一个包含用户提交评论内容的简单输入表单。因此,打开包含输入表单的 view文件并向下面这样简单的修改一下,就是这个protected/views/comment/_form.php:

<div class="form"> 
<?php $form=$this->beginWidget('CActiveForm', array(
    'id'=>'comment-form',
    'enableAjaxValidation'=>false, 
)); ?>
    <p class="note">Fields with <span class="required">*</span> are required.</p>
    <?php echo $form->errorSummary($model); ?>
    <div class="row"> 
        <?php echo $form->labelEx($model,'content'); ?> 
        <?php echo $form->textArea($model,'content',array('rows'=>6,'cols'=>50)); ?> 
        <?php echo $form->error($model,'content'); ?>
    </div>
 
    <div class="row buttons"> 
        <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' :'Save'); ?> 
    </div>
<?php $this->endWidget(); ?>
</div>

有了这一切,我们可以访问某个问题的页面,如:http://hostname/trackstar/index.php?r=issue/view&id=1

我们看到在页面底部的评论输入表单:

images/book1/chapter9/2.jpg

如果我们试图提交没有指定任何内容的评论,我们看到像下面截图所描述一个错误:

images/book1/chapter9/3.jpg

然后,如果我们登录一个账号为Test User One的用户,我们提交我的第一个评论:My first test comment,我们会看到下面的显示:

images/book1/chapter9/4.jpg

创建一个最新评论的挂件

现在,我们具有了留言的功能,我们将主要注意力放在第二个重要目标上。我们想要显示给用户一个穿插在所有页面左侧的,关于不同问题最新评论的表单。这将提供一个用户活跃程度的快照(译者注:这个快照应该说的是类似于进度条一样的图片,不过是每次访问的时候根据实际情况自动生成的)。我们也想要使用在内容区开辟一小块地方的方式,以此使得它在整个网站所有不同位置重复使用。这是在开发如新闻论坛、天气预报之类的web portal应用中很常见的风格,在雅虎和iGoogle也同样使用。这一小段内容经常在像portlets被提及,这也是为什么我们在本迭代开始的时候提到开发一个portlet的原因。另外,你可以输入http://en.wikipedia.org/wiki/Portlet查看更多相关的信息。

CWidget介绍

运气不错,Yii已经帮我们完成了它的结构。Yii为这个最终目的提供了一个叫做CWidget的组件类。一个Yii的widget就是这些类中的一个实例(或者是子类的实例),它是一个表面组件典型应用,镶嵌在一个视图文件里用来显示自足,可重用的用户界面样式。我们将使用一个Yii widget来建立一个最新内容portlet和在主要项目详情页面显示它所以我们能看见评论活跃度穿插在所有问题与项目联系起来。为了展示重用的方便性,我们将更进一步地在该项目的详细页面显示具体意见列表。

开始添加我们的widget,我们将首先在我们的评论AR模型类上添加一个新的公共方法以便返回大部分最近添加的内容。正如所料,我们先写一个测试程序。

但是在写这个test方法以前,让我们更新一下我们的评论固定的数据以便在我们整个测试过程中有几个评论内容供使用。在protect/tests/fixtures下创建一个新文件叫做tbl_comment.php。打开它添加以下内容:

<?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'
    ), 
);

现在我们有了一致的,可预计的和可重复的评论内容可供利用了。

创建一个新的unit测试文件protect/tests/unit/CommentTest.php并添加以下内容:

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

当然这个测试会失败,因为我们还没有加入Comment::findRecentComments() 方法到Comment模型类里。所以,我们现在来添加它。我们将继续添加我们所需的所有方法,而不是只添加可供测试应用成功运行的方法。但如果你是一直跟随下来的,你可以随时转移到你自己的步骤。打开Comment.php添加以下public static方法:

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

我们的新方法包含了两个可选参数,一个限制了评论的字数,其它的标记了评论所归属的特定项目的ID。第二个参数将允许我们使用我们新的挂件以显示一个项目详情页面所有项目的内容。所以,如果输入项目id被标记,否则,贯穿所有项目的所有内容都将被返回。

Yii中更多与AR关联查询相关的

上述两个AR关联查询对我们有点新。我们并没有在这之前使用过这些查询选项。以前我们一直在使用最简单的关联查询方法:

    1. 加载AR实例。
    2. 访问定义在relations()方法中的关联属性。

例如:如果我们想查询项目id为#1中的所有问题(issue),我们将执行以下两行代码:

// 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;

这个熟悉的方法采用的是延迟加载。当我们第一次创建该项目的实例时,该查询不返回相关的所有问题(issue)。它只是一个初步的查询,当执行$project->issues才会进一步确定相关的问题。这被叫做惰性查询,因为它会等待加载问题(issue)。

这种方法很方便,也非常有效,尤其是在不需要那些相关问题(issue)的情况下。但是,在其他情况下,这种方法效率可能有点低。例如,如果我们查询N个项目的问题(issue),使用这种惰性方式将执行N次连接查询。如果N很大,这可能非常低效。在这种情况下,我们选择另一种方式叫做预加载(Eager Loading)

预加载方式的关相AR实例同时也是主AR实例的请求。这是通过使用with()方法与find()或findAll()方法两者一起的AR查询。继续我们的项目实例,我们可以执行以代码,使用预加载方式立即查询所有项目的问题(issue):

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

在这种情况下,$projects数组中的每一个project AR实例的issues属性都已经被赋值了。这一结果仅使用了一个连接查询。

我们在findRecentComments()方法中使用了两种关联查询。一种是限制了获得的评论是在一个具体的项目(project)中。正如你看到的,我们在预加载问题(issue)时指定一个查询条件。让我们看看下面这行代码:

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

这个查询连接了tbl_comment表和tbl_issue表。继续project id等于#1这个例子,上面的AR关联查询基本执行的是类似下面的SQL语句:

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)

我们在findAll方法的参数数组中指定了一个排序(order)和限定(limit)的条件来执行SQL语句。

最后一点需要注意的是,在两个查询中我们是如何处理两个表相同列名的歧义。显示,当两个正在连接的表具有相同的列名,我们要在查询时,区别两者。就这个例子而言,两个表都定义了create_time列。我们尝试排序的列是在tbl_comment表中而不是定义在issue表中。在Yii的AR的关联查询中,对主表的别名被固定为t和,而对关联表的别名,默认情况下与关联名相同。因此,在我们这两个查询中,我们指定t.create_time表示我们要使用主表的列。如果我们想使用issue表的create_time列,我们将要修改例子中的第二个查询为如下这样:

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

好了,现在我们完全理解我们的新方法是干什么的了,在此基础上,我们需要一个完整的测试。为了充分测试新方法,我们需要为我们fixture(应该是指文件夹名字)数据做一些修改。打开每一个fixture里的文件:tbl_project.php,tbl_issue.phptbl_comment.php并且确保这些入口已经就位。

在tbl_project中加入以下代码:

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

在tbl_issue加入以下代码:

'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' => '',
),

最后,在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'=> '',
),

现在我们总计有三个评论在测试数据库中。其中两个分别与项目#1和#3相关。

现在我们可以改变以下我们的测试方法了:

    • 对所有评论发出请求
    • 限制返回的评论为两个
    • 限制只返回与#3相关的评论。

用下面的方法测试三个脚本

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);
}

我们还需要确保我们的CommentTest类为comments,issues和projects而使用fixture的数据。确保下面的代码添加到CommentTest类的开头:

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

现在,如果我们再运行这个测试程序,我们将得到6个要求被通过:

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

在Yii中,提供惰性加载和预加载的见解,我们应该针对IssueController::actionView()的操作方法里加载Issue模型部分作出调整。既然我们修改了issues的详细视图用来显示我们的评论和评论作者,这个操作方法将调用loadModel(),我们知道使用预加载提前载入我们的评论连同它们各自的作者。要作到这一点,我们可以为 loadModel()方法添加一个简单的标识,来表明这是否加载的评论。

像下面这样修改IssueController::loadModel() 方法:

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;
}

现在我们可以在IssueController::actionView() 修改这个方法的调用,像这样:

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

当一切就绪,我们将通过调用一次数据库载入所有的内容,以及它们各自作者的信息。

添加widget

现在我们准备添加新的挂件,以此使用新的方法来显示我们最近的内容。

正如我们前面提到的,在Yii里一个挂件是框架类CWidget或它的子类扩展的一个类。我们将在protected/components里添加一个新的挂件,作为这个文件夹的内容已被指定在主配置文件自动加载。这样,我们不会有明确导入的类,我们希望在每次需要的时候才使用它。我们将给我们的挂件命名为RecentComments,所有我们需要添加一个相同名字php文件。添加下面这个新创建的类定义到RecentComment:

<?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');
    }
}

主要工作是添加一个新的挂件来覆盖这个基类的init()和run()方法。init()方法初始化挂件并且在它的属性被初始化以后调用,Run()方法执行这个挂件。在这种情况下,我们通过调用基于$displayLimit and $projectId properties的最新内容初始化这个挂件。widget的执行本身只是呈现其相关的视图文件,这是我们还没有创建一些东西。视图文件,从习惯上是直接放入和挂件同一个文件夹的视图文件夹里,有着和挂件相同的名字,但是但以小写字母开始。按照惯例,创建一个新的文件,路径为protected/ components/views/recentComments.php。一旦创建,在该文件里添加如下标记:

<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>

这次调用RenderComments挂件的getRecentComments()方法是返回内容数组,并用叠加的方式展示了谁添加了评论和在评论上留下了相关问题。

为了按照顺序查看结果,我们需要在一个已有的控制器视图文件里嵌入这个挂件。如前所述,我们想要在项目列表里使用这个挂件,以显示所有项目的最近评论,仍然需要一个特定的项目的详细信息页面,以此为最近的评论显示特定的项目。

让我们开始完成项目列表页面吧。负责展示内容的视图文件是protected/views/project/index.php。打开这个文件在底部添加以下内容:

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

现在如果我们查看这个项目的列表页http://localhost/trackstar/index.php?r=project,我们会看到类似以下内容的截图:

images/book1/chapter9/5.jpg

我们现在只是通过调用挂件在页面内嵌入我们的新的评论,这很好,但是我们可以把小挂件更进一步用来显示并在一个与所有在这个应用里其它的portlets保持一致。我们可以通过Yii的另一个类的优势来提供给我们,这就是CPortlet。

CPortlet简介

CPortlet是zii的一部分,是将官方的扩展类库打包放在Yii里。它给所有的portlet风格的挂件提供了一个优秀的基类。它允许我们渲染一个好的标题和保持一致的HTML标记,因此所有的挂件可以非常轻松的以同样的方式加入到应用程序中。一旦我们有一个渲染内容的挂件(就像 RecentComments ),我们可以简单地使用所提供挂件作为CPortlet的一部分,CPortlet本身就是一个挂件,因为它也是扩展自CWidget。我们可以通过把 RecentComments挂件的调用放置在CPortlet的beginWidget()和一个endWiget()之间,就像下面这样:

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

由于CPortlet提供了一个标题属性,我们将其设置为对我们的portlet有用的东西。然后我们使用RecentComments挂件已渲染的内容来填充到portlet挂件里。最后的结果如下截图:

images/book1/chapter9/6.jpg

这不是自从我们做过什么以前一个巨大变化,但我们已经把内容放到了一个一致的已经被应用于整个站点的容器里。注意右边字段菜单内容块和我们最近创建的内容之间的相似处。我确定这将不会让你感到惊奇,右边菜单块也是包含在CPortlet容器内的。简单看一下protected/views/layouts/column2.php,这是一个我们最初创建应用时通过yiic命令自动生成的,留意下面的代码:

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

如此看来,应用已经利用了portlets。

在另一个页面加入我们的挂件

我们同样的加入portlet到项目详情页面,并且对那些和项目相关联的内容进行约束。

在protected/views/project/view.php的底部加入下列代码:

<?php $this->beginWidget('zii.widgets.CPortlet', array( 
    'title'=>'Recent Project Comments',
));
$this->widget('RecentComments', array('projectId'=>$model->id));
$this->endWidget(); ?>

还要在项目列表页面加入一些基本相同的东西,除了我们通过添加一个name=>value的数组对的调用来初始化这个RencentComments挂件的$projectID属性。

现在我们浏览特殊页面的详情,我们将看到类似下面这个截图:

images/book1/chapter9/7.jpg

这个截图显示了#3的项目,其中有一个相关的详细信息页面只有一个问题,关于这个问题的评论所描述的画面。您可能需要添加对这些问题的一些留言以产生类似的效果。我们现在可以在任何地方显示最近的一些评论,使用极少的配置参数易维护的方式。

小结

有了这个迭代,现在我们已经开始使用各种功能让Trackstar应用功能变得充实,已经达到今天大多数用户的期望。用户之间彼此互相联系的功能对于一个成功的问题管理系统是一个必不可少的组成部分。

当我们创造了这个必要的功能,我们能够深入研究如何写AR的关系查询。我们还介绍了了一个名为内容挂件的部件和Portlet。这个方法可以让我们用来开发小内容块,并有可能在网站的任何地方使用它们。这方法大大提高可重用性和一致性,并且便于维护。

在接下来的迭代中,我们将在这里创建基于最近的评论的挂件用来展现内容通过像RSS一样来产生,以此让用户来跟踪应用程序或项目的活跃情况而无需访问应用程序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值