第七天 模型与视图操作

Symfony回顾

现在我们的学习已经过去六天了,也许我们其中的一些人会认为到现在为止程序并不是十分的有用。这是因为一些人是通过可用的页面数量来评价一个程序是否有用的,而他们认为askeet只是显示一个问题列表,显示相关的答案以及处理用户会话。

我们并没有添加大量页面的原因是因为使用Symfony添加页面实在是太容易了。我们需要证明?好的。今天我们将会显示一个最后提问的问题列表,一个最后发表的答案列表,一个对某一个问题感兴趣的用户列表,用户的配置,并且我们会在每一个页面上添加一个浏览栏来访问这些特性。因为这些工作并不够一小时,我们同时会进行视图配置,并且会最终查看一下我们这周所完成的工作。准备好了?让我们开始吧。

重构

现在我们要使用一个与question/templates/_list.php相类似的页码控件添加一个页码列表。我们并不希望重复自己,所以我们要将这些页码代码放在一个自定义的帮助器中。帮助器是模板可以访问的一个PHP函数(就如同link_to()与format_date()帮助器)。

在askeet/apps/frontend/lib/helper中个GlobalHelper.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;
}

这个页面浏览帮助器改进了我们前面所编写的代码:他可以使用任意的路由规则,不会为第一个页面显示'previous'链接,同样也不会为最后一个页面显示'next'链接。我们同时添加了四个图片(first.gif,previous.gif,next.gif,last.gif)来使链接显示更漂亮。我们可以在以后的工程中重用这个帮助器。

要在question/templates/_list.php片段中使用这个帮助器,调用方法如下:

<?php use_helper('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>

名字Global指向我们刚刚创建的GlobalHelper.php文件。

测试我们的工作是否正常:
http://askeet/frontend_dev.php/

最近问题列表

在question模块中,创建下面的动作:

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()方法,这是因为我们知道模板需要知道问题作者的详细信息。这就意味着首先请求问题列表,然后对于每一个问题请求得到相关的用户。当我们调用:
$question->getUser();
doSelectJoinUser()方法会在一个请求中完成所有的工作。

这里并没有请求发往数据库。joinUser允许我们将请求数量由1+问题数量减少到1。数据库会庆幸这样简单的优化。

Propel文档会为这个特性提供详细的解释。

最近问题列表模板看起来与在主页显示的问题列表十分相似。用下面的代码创建askeet/apps/frontend/module/question/templates/recentSuccess.php文件:

<h1>recent questions</h1>

<?php include_partial('list', array('question_pager' => $question_pager)) ?>

现在我们就可以理解为什么在第五天的学习中将问题列表重构到一个代码片段中。最后,我们需要在frontend/config/routing.yml文件中添加recent_question规则:

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

但是需要求等一下:question/_list代码片段使用question/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中添加路由参数:

<h1>popular questions</h1>

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

要显示最近的问题列表,我们可以在我们的浏览器地址栏中输入:
http://askeet/question/recent

最近答案列表

这与上面的相类似,所以我们就可以直接操作:

创建answer模块

$ symfony init-module frontend answer

创建recent动作:

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

扩展AnswerPeer类:

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

创建新的recentSuccess.php模板:

<?php use_helper('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

现在我们已经习惯其用法了,不是吗?

用户配置

答案中的用户名字将会链接到我们所编写的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();
}

->getInterestsJoinQuestion()与->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(),->getAnswersJoinQuestion(),getQuestion()方法所返回的结果数量,以及排序方式。这可以通过覆写askeet/lib/model/User.php类文件中的相应方法来简单的做到,而我们在这里并讨论如何来做,但是今天的发布版本会包含相关的内容。

现在我们可以进行最终的测试了。让我们来看一下第一个用户所做的事情:

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

现在我们也可以由一个问题链接到一个用户配置。在question/templates/showSuccess.php以及question_body div开始处的question/templates/_list.php中添加下面的代码行:

<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帮助器。

添加浏览工具栏

我们将会改变全局的布局来添加一个侧边栏。这个侧边栏将会包含动态的内容,但是我们希望布局中可以设置其位置,他并不是每一个模板的一部分。另外,将侧边栏的代码放在模板中就意味着大量的重复,而正如我们所知的,我们并不会那样来做。

这就是为什么这个侧边栏是一个元素的原因。一个元素是一个动作的结果(例如,由模板执行所产生的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/frontend/modules/mymodule/config/目录下)。对于我们的情况来说,我们会为整个程序进行定义,并且在必须的时候进行覆写来在侧边栏中提供内容相关的链接。

所以打开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改为component.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>
<li><?php echo link_to('latest questions', 'question/recent') ?></li>
<li><?php echo link_to('latest answers', 'answer/recent') ?></li>
</ul>

如果现在我们试着在我们的askeet网站的每一个页面中进行浏览,那么我们就会得到一个错误。那是因为我们是在生产环境下浏览网站,而配置会进行缓存,并且在每次请求时并不会进行分析。我们修改了view.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关键字定义了在标题栏或是浏览器窗口所显示的标题。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

刷新当前页面。如果我们没有看到任何改变,那是因为我们是在开发环境下,那么我们应清除缓存来得到正确的窗口标题。

查看我们的工作

一个通常的传统是当我们到达第七天时应停下来查看一下我们的工作。这是一个记录当一些事情,包括当前的数据模型与可用的动作的好机会。

到目前为止,我们的工程中可用的动作列表如下:

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/目录下。

这七个小时显得并不坏,不是吗?

明天见

你可能感兴趣的:(工作,PHP,浏览器,cache,F#)