回顾
在我们第八天的学习中,我们很容易的为askeet添加了AJAX交互。现在程序已经相当有用了,但是还需要大量的小修改。在问题体中应允许丰富的文本,而主键不应出现在URI中。在Symfony中修正这些问题并不难,今天将是我们练习我们所学东西的一个好机会,并且可以检测我们是否已经知道如何操作MVC结构的所有层。
在question与answer上允许丰富的文本格式
Markdown
question与answer现在只允许接受普通文本。要允许基本的格式,粗体,斜体,超链接,图片,等,我们将会使用一个外部库,而不是重新发明轮子。
如果我们阅读过文本格式的Symfony文档,那么你也许知道我们是Markdown的fans。Markdown是一个文本到HTML的转换工具,以及一个文本格式化语法。例如,与Wiki或是论坛语法相比,Markdown的最大好处就是普通的markdown文本文件仍然具有可读性:
Test Markdown text
------------------
This is a **very simple** example of [Markdown][1].
The best thing about markdown is its _auto-escape_ feature for code chunks:
<a href="http://www.symfony-project.com">link to symfony</a>
>The `<` and `>` are properly escaped as `<` and `>`,
>and are not interpreted by any browser
[1]: http://daringfireball.net/projects/markdown/ "Markdown"
Markdown的渲染结果如下:
Test Markdown text
This is a very simple example of Markdown. The best thing about markdown is its auto-escape feature for code chunks:
<a href="http://www.symfony-project.com">link to symfony</a>
The < and > are properly escaped as < and >, and are not interpreted by any browser
Markdown库
尽管最初是使用Perl语言编写的,但是现在Markdown已经可以作为一个PHP库来下载了。而这正是我们要使用的。下载markdown.php文件,并将其放置在askeet工程的lib目录下(askeet/lib)。就是这样:如果在我们的文件中首先进行请求,那么现在askeet程序的所有类都可以访问这个库了:
require_once('markdown.php');
我们可以在每次要显示消息体时调用Markdown转换器,但是这会给我们的服务器造成沉重的负载。我们会在问题创建时将文本体转换为HTML,并且在Question表中存储内容的HTML版本。也许我们已经熟悉这样了,所以模块扩展并不惊奇。
扩展模块
首先在schema.xml中的Question表部分添加一列:
column name="html_body" type="longvarchar" />
然后,生成模块并且更新数据库:
$ symfony propel-build-model
$ symfony propel-build-sql
$ symfony propel-insert-sql
覆盖setBody方法
当调用Question类的->setBody()方法时,html_body列必须使用文本内容的Markdown版本进行更新。打开askeet/lib/model/Question.php模块文本,并且编写下面的函数:
public function setBody($v)
{
parent::setBody($v);
require_once('markdown.php');
// strip all HTML tags
$v = htmlentities($v, ENT_QUOTES, 'UTF-8');
$this->setHtmlBody(markdown($v));
}
在设置HTML内容之前使用htmlentities()函数可能保护askeet免受跨站点脚本(cross-site-scripting XSS)攻击,因为所有的<script>标记都会转义。
更新测试数据
我们会在问题的测试数据中添加一些Markdown格式(askeet/data/fixtures/test_data.yml),来检测转换可以正常工作:
Question:
q1:
title: What shall I do tonight with my girlfriend?
user_id: fabien
body: |
We shall meet in front of the __Dunkin'Donuts__ before dinner,
and I haven't the slightest idea of what I can do with her.
She's not interested in _programming_, _space opera movies_ nor _insects_.
She's kinda cute, so I __really__ need to find something
that will keep her to my side for another evening.
q2:
title: What can I offer to my step mother?
user_id: anonymous
body: |
My stepmother has everything a stepmother is usually offered
(watch, vacuum cleaner, earrings, [del.icio.us](http://del.icio.us) account).
Her birthday comes next week, I am broke, and I know that
if I don't offer her something *sweet*, my girlfriend
won't look at me in the eyes for another month.
我们现在可以重新装入数据库:
$ php batch/load_data.php
修改模板
question模块的showSuccess.php模板可以进行简单的修改:
...
<div class="question_body">
<?php echo $question->getHtmlBody() ?>
</div>
...
list模板片段(_list.php)也会显示内容,但是却是截短的形式:
<div class="question_body">
<?php echo truncate_text(strip_tags($question->getHtmlBody()), 200) ?>
</div>
现在可以测试所有的修改了:显示我们修改的三个页面,并且观察由测试数据所显示的格式化文本:
http://askeet/question/list
http://askeet/recent
http://askeet/question/show/stripped_title/what-shall-i-do-tonight-with-my-girlfriend
同样的修改也适用于Answer内容:在模块中创建一个html_body列,覆盖->setBody()方法,question/show所显示的another时使用的是->getHtmlBody()方法,而不是->getBody()方法。因这些代码与上面的完全相同,所以在这里我们不再进行描述,但是我们可以在SVN代码中看到修改。
隐藏所有的id
在Symfony中另一个好的习惯就是尽量避免将主键作为参数传递。这是因为我们的主键是自动增加的,这样就为破坏都提供了太多的关于数据库记录的信息。另外,这样显示的URI并没有任何意义,而且也不适用于搜索引擎。
例如,以用户配置页面为例。现在,他使用用户id作为参数。但是如果我们确保nickname是唯一的,那么他也可以是请求的参数。让我们进行修改。
更改动作
编辑user/show动作:
public function executeShow()
{
$this->subscriber = UserPeer::retrieveByNickname($this->getRequestParameter('nickname'));
$this->forward404Unless($this->subscriber);
$this->interests = $this->subscriber->getInterestsJoinQuestion();
$this->answers = $this->subscriber->getAnswersJoinQuestion();
$this->questions = $this->subscriber->getQuestions();
}
更改模块
在askeet/lib/model/目录下的UserPeer中添加下面的方法:
public static function retrieveByNickname($nickname)
{
$c = new Criteria();
$c->add(self::NICKNAME, $nickname);
return self::doSelectOne($c);
}
更改模板
现在到用户配置的显示链接必须关注用户的nickname而不是其id。
在question/showSuccess.php,question/_list.php模板中,将下面的代码:
<?php echo link_to($question->getUser(), 'user/show?id='.$question->getUserId()) ?>
替换为:
<?php echo link_to($question->getUser(), 'user/show?nickname='.$question->getUser()->getNickname()) ?>
对answer/_answer.php模板也要做同样的修改。
添加路由规则
在这个动作的转发配置中添加一条新的规则,从而url模式会显示一个nickname请求参数:
user_profile:
url: /user/:nickname
param: { module: user, action: show }
在执行Symfony的clear-cache命令之后,我们要做的最后一件事就是要测试我们的修改。
转发(routing)
除了今天的内容之外,我们到现在所编写的许多动作都使用默认的转发规则,从而模块的名字与动作会显示在浏览器的地址栏中。我们已经了解了如何进行修正,所以让我们为所有的动作定义URL模式。编辑askeet/apps/frontend/config/routing.yml:
# question
question:
url: /question/:stripped_title
param: { module: question, action: show }
popular_questions:
url: /index/:page
param: { module: question, action: list, page: 1 }
recent_questions:
url: /recent/:page
param: { module: question, action: recent, page: 1 }
add_question:
url: /add_question
param: { module: question, action: add }
# answer
recent_answers:
url: /recent/answers/:page
param: { module: answer, action: recent, page: 1 }
# user
login:
url: /login
param: { module: user, action: login }
logout:
url: /logout
param: { module: user, action: logout }
user_profile:
url: /user/:nickname
param: { module: user, action: show }
# default rules
homepage:
url: /
param: { module: question, action: list }
default_symfony:
url: /symfony/:action/*
param: { module: default }
default_index:
url: /:module
param: { action: index }
default:
url: /:module/:action/*
如果我们在生产环境中进行浏览,那么强烈建议在测试这些配置修改之前清除缓存。
Symfony转发的一个好习惯就是在一个link_to()帮助器中使用规则名,而不是module/action。不仅因为其速度快(转发引擎并不需要分析转发配置来查找合适的规则),还因为他允许我们在规则名之后修改动作。Symfony一书的转发一章进行详细的解释。
<?php link_to('@user_profile?id='.$user->getId()) ?>
// is better than
<?php link_to('user/show?id='.$user->getId()) ?>
Askeet遵循Symfony的好习惯,所以我们今天所下载的代码在链接帮助器中只有规则名。将所有模板中的action/module替换为@rule以及自定义帮助器并不是一件有趣的事情,所以关于转发的最后一条建议就是:当我们创建动作时编写转发规则,并且从开始就在链接帮助器中使用规则名。
明天见