原文:http://www.symfony-project.com/askeet/9
回顾: 在第八天里,我们毫不麻烦地为askeet添加了AJAX互动功能。应用程序现在很有用,但是还可以做一些小的改进。富文本应该可以在问题内容中得到使用,并且主键不应该在URI地址中出现。在symfony中实现这些功能并不是困难的事情: 今天将是一个很好的场合去实践我们已经学到的东西,并且去检查你所知道的怎样操作MVC架构中所有的层。
问题和答案的内容现在只能接受纯文本。为了允许基本的格式——bold, italic, hyperlinks, images,等等。——我们将使用一个外部库而不是重复发明轮子。 如果你已经看过文本格式的symfony文档,你可能已经了解我们是Markdown的粉丝。Markdown是一个文本到HTML的转换工具,并且是一种文本格式的语法。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最初是用Perl写成的,但是在PHP Markdown里仍然有它的PHP库可以使用。这个也就是我们将要使用的。下载markdown.php文件并放入askeet项目(askeet/lib/)的lib文件夹。一切就绪。现在Markdown对askeet应用程序的所有类都是可用的。请首先声明你的需求:
require_once('markdown.php');
每当我们显示消息内容的时候,我们就可以调用Markdown转换器。但是这样对我们的服务器会有很高的负载需求。我们最好在问题被建立的时候把文本内容转化成HTML内容,并且把HTML版的内容存入Question表内。你可能已经习惯了这种方式,因此模型扩展不会让你感到惊讶。
首先,在schema.xml中给Question表添加一列如下:
<column name="html_body" type="longvarchar" />
接下来,生成模型和跟新数据库:
$ symfony propel-build-model $ symfony propel-build-sql $ symfony propel-insert-sql
当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()函数。这样由于所有的<script>标签被忽略后,可以保护askeet免受跨网站执行码(cross-site-scripting XSS)的攻击。
我们将给测试数据(askeet/data/fixtures/test_data.yml)中的一些问题添加一些Markdown格式,这样可以检查Markdown的转换是不是正常工作:
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.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中展示的回答必须用→getHtmlBody()方法来代替→getBody()。由于这些代码和上面的修改是一模一样的,所以我们就不在这里列出了。但是你可以在今天的SVN代码中找到它。
另外一个在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命令之后,最后一件事就是去测试你的修改。
除了今天的增内容,许多动作直到现在还在使用默认的路由,因此模块名字和动作名字常常在浏览器的地址栏中被显示出来。你已经学会了怎样去修复它。因此让我们为所有的动作定义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好的实践,因此从今天最后教程中下载的代码在链接助手中仅仅包含了规则名字。在所有的模板和自定义助手中用@rule替换action/module不是一件快乐的事情,因此最后关于路由的建议是:当你创建动作的时候创建路由规则,并且从一开始就在链接助手中使用规则名称。
今天的改进在阅读上比要理解的内容多。此外,在教程中描述的修改在整个代码中相似的地方都被重复了。尽管没有真正新的特性在今天被加入,代码已经改变了很多。 如果你在今天感觉到并没有学到很多关于symfony的东西,这就意味着你已经快可以开始自己的项目了。创建一个动作,修改模型使它服务于动作所需,编写简单的模板输出动作和编辑配置整合新的动作到应用程序逻辑的过程是symfony开发的基础。 所有在此记录的好的实践(使用外部库替代在symfony中重写它,在应用程序中不要展示主键,使用路由规则名称替代module/action)将保持你的用程序干净,安全,快速和可维护。 但是askeet应用程序还远未完成!最紧缺的功能是添加新问题和新回答的能力。这就是我们明天要开发的。 你对第二十一天的附加特性有何建议?请确保发送它到askeet邮件列表。敬请关注后续内容!