本页目录

原文:http://www.symfony-project.com/askeet/8

模型和视图操作

回顾:

这已经是第六天了,可能有一些人认为这个应用程序迄今为止不是很有用。这是因为这些人认为一个应用程序只有几个网页可以用是没有用处的。他们看到askeet只能显示一个问题清单和对问题的回答,还有处理用户对话(session)。

我们没有重视网页数量的原因是用symfony添加新的网页时非常容易的。你想看到证明吗?好的,今天我们将展示一个最新问题的清单,一个最新答案的清单,和一个对某一问题感兴趣的用户清单,还有用户的信息。此外,我们将在每个页面上添加一个导航条去访问这些功能。 因为这些工作量对一个一个小时来说还不多,所以我们将设置视图配置文件并回顾一下我们在这周所做的工作。准备好了吗?开始吧。

重构

我们将像question/templates/_list.php中的分页控制一样,添加一个分页清单。我们并不喜欢重复我们工作,因此我们将从这个片断中提取分页代码放到一个自定义的助手(helper)中。助手是一个php函数,它用来被模版访问(就像link_to()和format_date()助手一样)。

在askeet/apps/frontend/lib/helper/中创建一个GlobaleHelper.php,并加入:

<?php
 
function pager_navigation($pager, $uri)
{
  $navigation = '';
 
  if ($pager->haveToPaginate())
  {  
    $uri .= (preg_match('/\?/', $uri) ? '&' : '?').'page=';
 
    // First and previous page
    if ($pager->getPage() != 1)
    {
      $navigation .= link_to(image_tag('first.gif', 'align=absmiddle'), $uri.'1');
      $navigation .= link_to(image_tag('previous.gif', 'align=absmiddle'), $uri.$pager->getPreviousPage()).'&nbsp;';
    }
 
    // Pages one by one
    $links = array();
    foreach ($pager->getLinks() as $page)
    {
      $links[] = link_to_unless($page == $pager->getPage(), $page, $uri.$page);
    }
    $navigation .= join('&nbsp;&nbsp;', $links);
 
    // Next and last page
    if ($pager->getPage() != $pager->getCurrentMaxLink())
    {
      $navigation .= '&nbsp;'.link_to(image_tag('next.gif', 'align=absmiddle'), $uri.$pager->getNextPage());
      $navigation .= link_to(image_tag('last.gif', 'align=absmiddle'), $uri.$pager->getLastPage());
    }
 
  }
 
  return $navigation;
}

分页导航助手改善了我们以前写的代码。他能使用任何路由规则,在第一页上不显示“前一页”链接同时在最后一页也不显示“后一页”链接。我们同样也添加了四个图片(first.gif, previouse.gif,next.gif和last.gif)从而使链接看起来更好。你可以从askeet SVN版本库中的到他们。你可能价格重用这个助手在将来你自己的项目中。

为了在question/templates/_list.php中使用这个助手,调用助手函数如下:

<?php use_helpers('Text', 'Global') ?>
 
<?php foreach($question_pager->getResults() as $question): ?>
  <div class="question">
    <div class="interested_block">
      <?php include_partial('interested_user', array('question' => $question)) ?>
    </div>
 
    <h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>
 
    <div class="question_body">
      <?php echo truncate_text($question->getBody(), 200) ?>
    </div>
  </div>
<?php endforeach; ?>
 
<div id="question_pager">
  <?php echo pager_navigation($question_pager, 'question/list') ?>
</div>

注意在一开始user_helpers()名字中的那个“s”,这是因为我们加入了超过一个的助手。名字Global引用了我们创建的GlobalHelper.php文件。

就像以前一样,用申请检查每件事是否工作:

http://askeet/frontend_dev.php/

列出最新问题

在question模块中,创建新动作recent:

public function executeRecent()
{
  $this->question_pager = QuestionPeer::getRecentPager($this->getRequestParameter('page', 1));
}

非常简单。我们认为攫取最新问题的功能应该是QuestionPeer类的方法。-Peer的类被设计用来返回一个给定类的实例清单——在symfony书模型一章中有详细解释。但是getRecent()方法仍然需要被创建。打开askeet/lib/model/QuestionPeer.php类并添加:

public static function getRecentPager($page)
{
  $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));
  $c = new Criteria();
  $c->addDescendingOrderByColumn(self::CREATED_AT);
  $pager->setCriteria($c);
  $pager->setPage($page);
  $pager->setPeerMethod('doSelectJoinUser');
  $pager->init();
 
  return $pager;
}

创建时间降序的方式可以选出最新的问题。这个方法使用了self替代parent是因为这是个类函数,而不是对象函数。我们用doSelectJoinUser()替代简单doSelect()的原因是我们知道模版将需要问题作者的详细内容。这就意味着,第一个申请用来查询问题清单,再加上对每个问题的申请来得到相关的用户。doSelectJoinUser()方法做了所有这个一切只用了一个申请:当我们调用

$question->getUser();

。。。这里不再有对数据的申请了。joinUser允许我们把申请的数量由“1+问题数量”减少到了仅仅一次申请。数据库应该感谢我们这个简单的优化。 Propel文档将会给你关于这个特点的所有解释。 列出最新问题的模版看起来非常象在主页中列出问题的清单。创建askeet/apps/frontend/module/question/templates/recentSuccess.php:

<h1>recent questions</h1>
 
<?php include_partial('list', array('question_pager' => $question_pager)) ?>

你现在明白了为什么我们在第五天把问题清单重构到一个片断中。最终,你需要添加一个recent_question规则在frontend/config/routing.yml配置文件中,就像第四天的一样:

recent_questions:
  url:   /recent/:page
  param: { module: question, action: recent, page: 1 }

但是请等一下:question/_list片段创建了链接路由规则questin/list的链接,因此使用他是无法工作的对于最新的问题清单来说。我们需要把路由规则作为参数传递个代码从而使它可以被不同的页面重用。因此改变recentSuccess.php的最后一行代码为:

<?php include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/recent')) ?>

同样改变_list.php片段的最后一行为:

<div id="question_pager">
  <?php echo pager_navigation($question_pager, $rule) ?>
</div>

不要忘了同样添加规则参数在modules/question/templates/listSuccess.php文件里对_list的调用中。

<h1>popular questions</h1>
 
<?php echo include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/list')) ?>

清空缓存(因为配置文件被修改了),很简单。 显示最新问题的清单,在你浏览器的地址栏输入:

http://askeet/recent

列出最新回答

这和上面就是一回事,因此我们这次非常直截了当:

$symfony init-module frontend answer
public static function getRecentPager($page)
{
  $pager = new sfPropelPager('Answer', sfConfig::get('app_pager_homepage_max'));
  $c = new Criteria();
  $c->addDescendingOrderByColumn(self::CREATED_AT);
  $pager->setCriteria($c);
  $pager->setPage($page);
  $pager->setPeerMethod('doSelectJoinUser');
  $pager->init();
 
 
  return $pager;
}
<?php use_helpers('Date', 'Global') ?>
 
 
<h1>recent answers</h1>
 
 
<div id="answers">
<?php foreach ($answer_pager->getResults() as $answer): ?>
  <div class="answer">
    <h2><?php echo link_to($answer->getQuestion()->getTitle(), 'question/show?stripped_title='.$answer->getQuestion()->getStrippedTitle()) ?></h2>
    <?php echo count($answer->getRelevancys()) ?> points
    posted by <?php echo link_to($answer->getUser(), 'user/show?id='.$answer->getUser()->getId()) ?> 
    on <?php echo format_date($answer->getCreatedAt(), 'p') ?>
    <div>
      <?php echo $answer->getBody() ?>
    </div>
  </div>
<?php endforeach ?>
</div>        
 
 
<div id="question_pager">
  <?php echo pager_navigation($answer_pager, 'answer/recent') ?>
</div>
http://askeet/answer/recent

你已经开始习惯测试了,不是吗?

注意:对那些看过第四天的人来说可能注意到了这段展示问题详细信息的代码。因为这段代码被使用在了至少两个地方,因此我们将重构它并且建立一个_answer.php片段,它将被在question/show和answer/recent中使用。细节可以在askeet SVN版本库中找到。

用户信息

在回答中的用户名将连接到user/show动作。这将是用户的详细信息,并且显示这个用户所提出的问题和做出的回答。 第一件需要做的事是建立一个动作:

public function executeShow()
{
  $this->subscriber = UserPeer::retrieveByPk($this->getRequestParameter('id', $this->getUser()->getSubscriberId()));
  $this->forward404Unless($this->subscriber);
 
  $this->interests = $this->subscriber->getInterestsJoinQuestion();
  $this->answers   = $this->subscriber->getAnswersJoinQuestion();
  $this->questions = $this->subscriber->getQuestions();
}

→getInteretsJoinQuestion()和→getAnswersJoinQuestion()方法是User类的内建方法。你可以到askeet/lib/model/om/BaseUser.php类中去查看他们是怎么工作的。 askeet/apps/frontend/modules/user/templates/showSuccess.php模版将不会给你任何问题:

<h1><?php echo $subscriber ?>'s profile</h1>
 
<h2>Interests</h2>
 
<ul>
<?php foreach ($interests as $interest): $question = $interest->getQuestion() ?>
  <li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>
<?php endforeach; ?>
</ul>
 
<h2>Contributions</h2>
 
<ul>
<?php foreach ($answers as $answer): $question = $answer->getQuestion() ?>
  <li>
    <?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?><br />
    <?php echo $answer->getBody() ?>
  </li>
<?php endforeach; ?>
</ul>
 
<h2>Questions</h2>
 
<ul>
<?php foreach ($questions as $question): ?>
  <li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>
<?php endforeach; ?>
</ul>

当然,你希望限制User对象里→getInterestsJoinQuestion(), →getAnswerJoinQuestion()和getQuestions()方法返回结果的数量,还有排列好的顺序。用重载在askeet/lib/model/User.php类文件中的函数很容易实现它,在这里我们不将谈论怎么实现了,但是在今天的版本里面由这个代码。 是时间做最后的测试了。让我们看看你第一个用户做的:

http://askeet/user/show/id/1

现在我们可以从问题连接到用户信息里。添加下列代码到question/templates/showSuccess.php和question/templates/_list.php文件中question_body div的开始位置:

<div>asked by <?php echo link_to($question->getUser(), 'user/show?id='.$question->getUser()->getId()) ?> on <?php echo format_date($question->getCreatedAt(), 'f') ?></div>

不要忘了在_list.php中声明Date助手的使用。

添加导航条

我们将改变全局布局去添加一个侧面条。这个侧面条将包含动态的内容,但是由于我们想把它至于布局中,因此他不能是每个模版的一部分。此外,把这个侧面条的代码放到模版中意味着重复多次。你知道我们不喜欢这样做。 这就是为什么这个条将会是一个组件(component)。一个组件是一个动作的结果(例如,从模版执行得来的HTML)并且可以在不同情况下使用。Symfony书中视图一章解释了组件和组件于片段的不同。

添加组件到布局中

打开全局布局(askeet/apps/frontend/templates/layout.php)。你还记这这部分代码吗?

<div id="content_bar">
  <!-- Nothing for the moment -->
  <div class="verticalalign"></div>
</div>

用以下代码替换注释语句

<?php include_component_slot('sidebar') ?>

就是它了。

定义组件中使用的方法

我们决定使用比简单组件更强大的:组件插槽。这是一个动作可以根据调用函数被修改的组件,他允许上下文内容。下面是视图配置(在一个view.yml文件中),他定义了与组件插槽相对应的动作:

default:
  components:
    sidebar:      [sidebar, default]

在这个例子中,名为sidebar的组件插槽被声明作为sidebar模块默认动作的返回结果。 这个视图配置可以被定义成整个应用程序范围(在askeet/apps/frontend/config/目录中)或者是某个特定的模块(在askeet/apps/frontend/modules/mymodule/cofig目录中。在我们的例子中,我们把它定义为整个应用程序范围,并且重写它在需要在边栏中显示显示上下文相关内容时。 因此,打开askeet/apps/frontend/config/view.yml并添加以上组件插槽配置。你可以找到更多关于视图配置信息在symfony书中相关的章节

编写sidebar/default动作和模版

首先,我们用symfony初始化新的sidebar模块:

$ symfony init-module frontend sidebar

接下来,我们需要编写一个默认的组件。在askeet/apps/frontend/modules/sidebar/actions目录下,重新命名actions.class.php类为components.class.php,并且修改他的内容为:

<?php 
 
class sidebarComponents extends sfComponents
{
  public function executeDefault()
  {
  }
}

一个组件试图是一个模版,就像为其动作工作一样。不同地方是命名:组件试图被像片段一样命名(用_开始)而不是像模版(用Success结束)。因此创建一个askeet/apps/frontend/modules/sidebar/templates/_default.php片段(并且删除不会在被使用的indexSuccess.php),并添加以下内容:

<?php echo link_to('ask a new question', 'question/add') ?>
 
<ul>
  <li><?php echo link_to('popular questions', 'question/list') ?>
  <li><?php echo link_to('latest questions', 'question/recent') ?></li>
  <li><?php echo link_to('latest answers', 'answer/recent') ?></li>
</ul>

如果你现在尝试在你的askeet网站中的页面使用导航,你会的到一个错误信息。这是因为你是在产品环境中导航。在这个环境中,配置被缓存了,并且对于每个申请并没有解析。我们修改了viewl.yml配置文件,但是在产品环境中的动作并不知道。他们使用缓存后的版本——这个版本并不包含组件插槽配置。如果你想看到改变后的结果,或者清空缓存或者在开发环境中导航:

$symfony clear-cache

或者

http://askeet/frontend_dev.php/

导航条被正确的显示出来了

注意:这是一个普遍的产品环境配置影响。因此你需要记住在开发阶段使用开发环境(此时你需要大量的改变配置),并且在改变了配置之后,当你在产品环境中浏览网站需要清空缓存

再多一点视图配置

既然我们已经打开了它,就让我们看看在apps/config中的应用程序级的view.yml配置文件:

default:
  http_metas:
    content-type: text/html; charset=utf-8

  metas:
    title:        symfony project
    robots:       index, follow
    description:  symfony project
    keywords:     symfony, project
    language:     en

  stylesheets:    [main, layout]

  javascripts:    []

  has_layout:     on
  layout:         layout

  components:
    sidebar:      [sidebar, default]

metas这一部分包含了整个网站的meta标签的配置。title关键词同样定义了浏览器窗口标题栏中显示的标题。标题是非常重要的,因为这是用户第一眼看到的东西当网站被搜索索引发现时。因此,把它改为一些更合适askeet网站的内容是非常必要的:

  metas:
    title:        askeet! ask questions, find answers
    robots:       index, follow
    description:  askeet!, a symfony project built in 24 hours
    keywords:     symfony, project, askeet, php5, question, answer
    language:     en

刷新当前页。如果你没有看到任何改变,这是因为你在产品环境中。因此你需要先清空缓存,然后就能的到一个合适的窗口标题:

注意:Symfony除了为你的项目网页提供一个默认的标题,他还在web根目录(askeet/web)下创建了一个默认的robots.txt和favicon.ico。 同样,不要忘了更改他们!

注意:你可能需要针对每一个你网站的网页改变标题。你可以通过为每一个模块定义一个自定义的view.yml配置文件来实现,但是这个只能给你一个静态的标题。另一种方法是你可以使用一个动作的→setTitle()方法的动态值,就像在视图配置一章描述的那样:

php]
  $this->getResponse()->setTitle($title);

看一下我们做了些什么

这是一个普遍的传统——停下来然后看一看你做了些什么当你到达第七天的时候。这是一个好机会来记录一些事情,包括当前的数据模型和可以使用的动作。 事实上,你应该文档化你的代码当你写他们的时候,例如对每个方法使用PHP doc-style注释。在symfony项目中,方法或是函数使用的名字常常是他们的目的还有用法的说明。方法的内容被保持的很短,因此他是容易阅读的。大多数时间,模版仅仅使用自我解释的foreach和if语句。这就是为什么你发现在askeet SVN版本库中的代码并没有包含太多的文档,此外事实是我们已经写了七个小时的内容关于我们做了些什么! 现在让我们看一下更新后的实体关系图:

可以使用的动作的清单如下:

answer/
  recent
question/
  list
  show
  recent
sidebar/
  default (component)
user/
  show
  login
  logout
  handleErrorLogin

模型同样包含了如下的方法:

Anwser()
  getRelevancyUpPercent()
  getRelevancyDownPercent()
AnswerPeer::
  getRecentPager()
Interest->
  save()
Question->
  setTitle()
QuestionPeer::
  getQuestionFromTitle()
  getHomepagePager()
  getRecentPager()
Relevancy
  save()
User->
  __toString()
  setPassword()

myUser->
  signIn()
  signOut()
  getSubscriberId()
  getSubscriber()
  getNickName()

另外,一个自定义的工具类和一个自定义的验证器被置于askeet/apps/frontend/lib/目录下。 对七个小时来说还不算太坏,不是吗?

明天见

今天应用程序有了很大的进展,并且做的相当快。现在每件事都准备着在人机交互中加入一些AJAX。明天用户将能用ajax登录和声明他们对问题的关注。不要错过它! 你仍然可以用从askeet SVN版本库标签为release_day_7中下载今天所有的代码。askeet邮件列表将会比光速还快地回答你任何问题。