[原创中文翻译]symfony askeet24:第四天,重构。

September 30, 2007 – 7:24 pm

[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]

第四天,重构。

Show the answers to a question

显示问题答案

First, let’s continue the adaptation of the templates generated by the Question CRUD during day two

首先,继续修改前两天Question CRUD生成的模板。

The question/show action is dedicated to display the details of a question, provided that you pass it an id. To test it, just call :

question/show 动作主要显示question详情,传递ID,测试一下:

http://askeet/frontend_dev.php/question/show/id/1

question detail

问题详情

You probably already saw the show page if you played with the application before. This is where we are going to add the answers to a question.

如果你以前用过这些网站程序你应该已经看过每一个页面了。现在我们该给问题增加答案了。

A quick look at the action

action简单看一下

First, let’s have a look at the show action, located in the askeet/apps/frontend/modules/question/actions/actions.class.php file:

看一下show动作,见askeet/apps/frontend/modules/question/actions/action.class.php:

public function executeShow()
{
$this->question = QuestionPeer::retrieveByPk($this->getRequestParameter('id'));
$this->forward404Unless($this->question);
}

If you are familiar with Propel, you recognize here a simple request to the Question table. It is aimed to get the unique record having the value of the id parameter of the request as a primary key. In the example given in the URL above, the id parameter has a value of 1, so the ->retrieveByPk() method of the QuestionPeer class will return the object of class Question with 1 as a primary key. If you are not familiar with Propel, come back after you’ve read some documentation on their website.

如果熟悉propel,你应该能看出这里是一个针对Question表的简单请求。用主键id的值作为请求的变量值。文章开始我们访问的那个URL里,id初始值是1,所以QuestionPeer类的->retrieveByPk()方法返回Question类并把1当作主键。如果你不熟悉propel,好好去官方网站看看。

The result of this request is passed to the showSuccess.php template through the $question variable.

结果通过变量$question传递到showSuccess.php模板。

The ->getRequestParameter(’id’) method of the sfAction object gets… the request parameter called id, whether it is passed in a GET or in a POST mode. For instance, if you require:
http://askeet/frontend_dev.php/question/show/id/1/myparam/myvalue
…then the show action will be able to retrieve myvalue by requesting $this->getRequestParameter(’myparam’).

sfAction的->getRequestParameter()方法获得……id变量,不管是GET或POST传递。例如,如果你请求这个 URL:http://askeet/frontend_dev.php/question/show/id/1/myparam/myvalue

……那么show action会调用$this->getRequestParameter(’myparam’)取回myvalue。

[译者:这里的$this->getRequestParameter(’myparam’);实际上就等于$_POST[’myparam’],symfony之所以必须封装了GET和POST方式是因为symfony对于URL使用了路由协议,对于这样的URL:http://askeet/frontend_dev.php/question/show/id/1/myparam/myvalue

传统的$_POST是无法使用的。刚开始可能觉得这样很多余,但是我们想一下,传统的phpmvc模式开发出来的项目中,url格式一般为:

http://askeet/frontend/index.php?m=member&a=list&id=1

这样的url似乎很不友好,而且完美的要求是进行特殊处理。而在这里symfony都给我们做好了,我们需要做的仅仅是换一种代码表现形式就是,何乐而不为呢?]

The forward404Unless() method sends to the browser a 404 page if the question does not exist in the database. It’s always a good pratice to deal with edge cases and errors that can occur during execution and symfony gives you some simple methods to help you do the right thing easily.

如果请求的question不存在于数据库,forward404Unless()方法发送给浏览器404错误页。这是处理一些突发事件和错误问题的好办法。

Modify the showSuccess.php template

修改showSuccess.php模板

The generated showSuccess.php template is not exactly what we need, so we will completely rewrite it. Open the frontend/modules/question/templates/showSuccess.php file and replace its content by:

脚手架生成的showSuccess.php模板并不完全是我们想要的,所以要完全重新写。打开frontend/modules/question/templates/showSuccess.php把代码全部换成:

<?php use_helper('Date') ?>
<div class="interested_block">
<div class="interested_mark">
<?php echo count($question->getInterests()) ?>
</div>
</div>
<h2><?php echo $question->getTitle() ?></h2>
<div class="question_body">
<?php echo $question->getBody() ?>
</div>
<div id="answers">
<?php foreach ($question->getAnswers() as $answer): ?>
<div class="answer">
posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?>
on <?php echo format_date($answer->getCreatedAt(), 'p') ?>
<div>
<?php echo $answer->getBody() ?>
</div>
</div>
<?php endforeach; ?>
</div>

You recognize here the interested_block div that was already added to the listSuccess.php template yesterday. It just displays the number of interested users for a given question. After that, the markup also looks very much like the one of the list, except that there is no link_to on the title. It is just a rewriting of the initial code to display only the necessary information about a question.

你记得昨天我们把interested_block层加到listSuccess.php中吗?页面显示当前问题有多少个用户表示感兴趣。后边,高亮的部分看起来像列表而且标题还没加链接。把原先的代码改一下,只显示question的必要信息。

The new part is the answers div. It displays all the answers to the question (using the simple $question->getAnswers() Propel method), and for each of them, shows the total relevancy, the name of the author, and the creation date in addition to the body.

这里只有answer层是新内容。它显示问题所有答案(使用Propel的$question->getAnswers()方法),每一个答案都列出主要关系内容,答案提交者,创建日期。

The format_date() is another example of template helpers for which an initial declaration is required. You can find more about this helper’s syntax and other helpers in the internationalization helpers chapter of the symfony book (these helpers speed up the tedious task of displaying dates in a good looking format).

format_data()是symfony template helpers提供的另一个处理时间的方法。在symfony book里有更多介绍。

Propel creates method names for linked tables by adding an ’s’ automatically at the end of the table name. Please forgive the ugly ->getRelevancys() method since it saves you several lines of SQL code.

propel在数据表名后面自动加“s”来命名方法名。请原谅丑陋的->getRelevancys()方法吧,它能帮你省几行sql代码。

Add some new test data

加点新测试数据

It is time to add some data for the answer and relevancy tables at the end of the data/fixtures/test_data.yml (feel free to add your own):

需要给answer表和relevancy表增加点数据了,把下面的内容写在data/fixtures/test_data.yml后面(你可以自由添加你喜欢的内容):

Answer:
a1_q1:
question_id: q1
user_id: francois
body: |
You can try to read her poetry. Chicks love that kind of things.

a2_q1:
question_id: q1
user_id: fabien
body: |
Don’t bring her to a donuts shop. Ever. Girls don’t like to be
seen eating with their fingers - although it’s nice.

a3_q2:
question_id: q2
user_id: fabien
body: |
The answer is in the question: buy her a step, so she can
get some exercise and be grateful for the weight she will
lose.

a4_q3:
question_id: q3
user_id: fabien
body: |
Build it with symfony - and people will love it.

Reload your data with:

重新载入数据:

$ php batch/load_data.php

Navigate to the action showing the first question to check if the modifications were successful:

看看是否成功了:

http://askeet/frontend_dev.php/question/show/id/XX

Replace XX with the current id of your first question.

把XX换成你想要的第一个问题的id。

http://www.symfony-project.com/images/askeet/1_0/answers_day4.gif

The question is now displayed in a fancier way, followed by the answers to it. That’s better, isn’t it?

现在问题内容显示的方式不错,后面还有对应的答案。不错吧,是不是?

Modify the model, part I

第一部分,修改model。

It is almost certain that the full name of an author will be needed somewhere else in the application. You can also consider that the full name is an attribute of the User object. This means that their should be a method in the User model allowing to retrieve the full name, instead of reconstructing it in an action. Let’s write it. Open the askeet/lib/model/User.php and add in the following method:

程序有必要显示作者全名。你也可以理解把全名功能属于User对象属性。这意味着User model里该有个方法能取回全名,不要在action里重建方法。我们开始写,打开askeet/lib/model/User.php,在后面加个方法:

public function __toString()
{
return $this->getFirstName().' '.$this->getLastName();
}

Why is this method named __toString() instead of getFullName() or something similar? Because the __toString() method is the default method used by PHP5 for object representation as string. This means that you can replace the posted by <?php echo $answer->getUser()->getFirstName().’ ‘.$answer->getUser()->getLastName() ?> line of the askeet/apps/frontend/modules/question/templates/showSuccess.php template by a simpler posted by <?php echo $answer->getUser() ?> to achieve the same result. Neat, isn’t it ?

我们命名方法名为什么用__toString()呢?而不用getFullName()或者其他相似的名字呢?因为__toString()函数是PHP5中写对象字符串的默认函数。意思是你可以把这行代码<?php echo $answer->getUser()->getFirstName().’ ‘.$answer->getUser()->getLastName() ?> (见askeet/apps/frontend/modules/question/templates/showSuccess.php文件)替换为一简单的代码<?php echo $answer->getUser() ?>来达到同样的效果。牛吧?

Don’t repeat yourself

不要重复

One of the good principles of agile development is to avoid duplicating code. It says “Don’t Repeat Yourself” (D.R.Y.). This is because duplicated code is twice as long to review, modify, test and validate than a unique encapsulated chunk of code. It also makes application maintenance much more complex. And if you paid attention to the last part of today’s tutorial, you probably noticed some duplicated code between the listSuccess.php template written yesterday and the showSuccess.php template:

敏捷开发的好习惯是避免重复代码。叫“Don’t Repeat Yourself”(D.R.Y)。重复的代码又要看一遍,改一遍,测试和验证一遍,不比封装好的代码好。重复代码也让程序维护更加复杂。如果你仔细看看今天最后的教程,你可以在listSuccess.php模板中发现昨天写的重复代码,同样也存在于showSuccess.php模板。

<div class="interested_block">
<div class="interested_mark">
<?php echo count($question->getInterests()) ?>
</div>
</div>

[译者:为了和官方中文翻译《The Definitive Guide to symfony 中文版》同步,这里一些单词的翻译采相同的中文翻译内容。具体见下:

Code Fragments:代码片断

Partials:模板片断

Components:组件

Slots:槽

在这里我摘抄《The Definitive Guide to symfony 中文版》 中的一段话(只摘抄部分,希望没有版权问题):

symfony提供了三种不同的代码片段来取代include(Code Fragments:代码片断):

1.如果逻辑部分代码量很小,只需要包含一个能访问一些你传递的数据的模板。这样,你需要用局部模板(partial)。

2.如果逻辑的代码量比较大 (例如,你需要访问数据模型,并且根据session修改数据),你可能会想把逻辑与表现分开。这种情况,你需要用组件(component)。

3.如果这个片段用来替换布局里的特定部分,这个部分有一个默认的内容。你需要用槽(slot)。

注意:还有一种代码片段,组件槽(component slot),它用于代码片段与环境有关的情况(例如,对一个一个模块内的不同动作,这段代码需要有所不同)。

]

So our first session of refactoring will rmove this chunk of code from the two templates and put it in a fragment, or reusable chunk of code. Create an _interested_user.php file in the askeet/apps/frontend/modules/question/template/ directory with the following code:

所以我们重新修饰页面的第一部分内容就是祛除这两个模板中的重复代码,把这些代码放到(代码)片断里,重用重复代码。建立_interested_user.php文件(askeet/apps/frontend/modules/question/template/),加上下面的代码:

<div class="interested_mark">
<?php echo count($question->getInterests()) ?>
</div>

Then, replace the original code in both templates (listSuccess.php and showSuccess.php) with:

接着,替换掉那两个模板文件里原来的代码(listSuccess.php和showSuccess.php)为下面的代码:

<div class="interested_block">
<?php include_partial('interested_user', array('question' => $question)) ?>
</div>

[译者:include_partial官方翻译为模板片断,也可以叫局部模板。看了这幅图片相信你就明白了其中的意思,图片如下:

include_partial

如此listSuccess.php和showSuccess.php两个模板里都可以调用这个局部模板_interested_user.php。]

A fragment doesn’t have native access to any of the current objects. The fragment uses a $question variable, so it has to be defined in the include_partial call. The additional _ in front of the fragment file name helps to easily distinguish fragments from actual templates in the template/ directories. If you want to learn more about fragments, read the view chapter of the symfony book.

(代码)片断对当前对象没有接口。(代码)片断使用了$question变量,所以在使用include_partial时必须先定义。(代码)片断文件名称前面附加的“_”帮助你轻易和template目录下其他模板区别开来。如果你想知道更多关于(代码)片断的知识,看一下symfony book。

Modify the model, part II

第二部分,修改model。

The $question->getInterests() call of the new fragment does a request to the database and returns an array of objects of class Interest. This is a heavy request for just a number of interested persons, and it might load the database too much. Remember that this call is also done in the listSuccess.php template, but this time in a loop, for each question of the list. It would be a good idea to optimize it.

$question->getInterests()方法调用新的(代码)片断,功能是对数据库请求数据和返回一个Interest对象的数组。这是对我们得到感兴趣用户数量的重要请求操作,可能需要数据库运算多次。记住在listSuccess.php中也有这个功能,但是现在是循环,每一个问题都要得出感兴趣用户的数量来显示。完善一下这个功能是个好主意。

[译者:看到这里,你看明白了吗?

要是看明白了,接着看下面的教材。

要是没看明白,看我给你讲解一下。这句代码:

<?php echo count($question->getInterests()) ?>等于执行了

select count(*) as count from ask_interest where question_id=’{$question->getId()}’

然后得到感兴趣读者的数量。 askeet的作者觉得这样似乎效率太低,所以就想出了下面的方法。呵呵,你是不是和他想到一起了呢?]

One good solution is to add a column to the Question table called interested_users, and to update this column each time an interest about the question is created.

第一个好的解决方法是给Question表加个interested_users字段,每当一个用户对此问题表示感兴趣了,这个字段就更新一次。

We are about to modify the model without any apparent way to test it, since there is currently no way to add Interest records through askeet. You should never modify something without any way to test it.

我们修改model却没有办法测试,因为通过askeet现在的功能没有方法来添加感兴趣用户数量的记录。正常的原则是如果这个功能无法测试,就不要去做修改程序。

Luckily, we do have a way to test this modification, and you will discover it later in this part.

幸运的是,我们有方法测试这个修改,你将在后面发现。

Add a field to the User object model

给User object model增加一个字段(实际上就是个问题表加个字段)。

Go without fear and modify the askeet/config/schema.xml data model by adding to the ask_question table:

不要害怕,尽管去修改askeet/config/schema.xml,添加字段到ask_question表中:

<column name="interested_users" type="integer" default="0" />

Then rebuild the model:

接着重建model:

$ symfony propel-build-model

That’s right, we are already rebuilding the model without worrying about existing extensions to it! This is because the extension to the User class was made in the askeet/lib/model/User.php, which inherits from the Propel generated askeet/lib/model/om/BaseUser.php class. That’s why you should never edit the code of the askeet/lib/model/om/ directory: it is overridden each time a build-model is called. Symfony helps to ease the normal life cycle of model changes in the early stages of any web project.

对了,我们不要顾虑我们写的那些扩展程序,尽管重建model就是。因为对User类的扩展写在askeet/lib/model/User.php中,它继承了propel生成的askeet/lib/model/om/BaseUser.php类。这就是为什么你不要修改askeet/lib/model/om/下的代码:每当build-model命令被使用,om/下的代码文件就被重写一次。symfony帮助减轻早期web项目由于model改变而不断修改代码的周期。

You also need to update the actual database. To avoid writing some SQL statement, you should rebuild your SQL schema and reload your test data:

你还需要修改当前数据库。我们不想写sql语句,你需要重建sql和重新载入测试数据:

$ symfony propel-build-sql
$ mysql -u youruser -p askeet < data/sql/schema.sql
$ php batch/load_data.php

There is more than one way to do it. Instead of rebuilding the database, you can add the new column to the MySQL table by hand:

还有很多别的方法可以实现这个功能,而不需要重建数据库,我们手动给mysql增加一个新字段即可:

$ mysql -u youruser -p askeet -e "alter table ask_question add interested_users int default '0'"

Modify the save() method of the Interest object

修改Interest对象的save()方法

Updating the value of this new field has to be done each time a user declares its interest for a question, i.e. each time a record is added to the Interest table. You could implement that with a trigger in MySQL, but that would be a database dependent solution, and you wouldn’t be able to switch to another database as easily.

每当一个用户对某一问题表示感兴趣时,数据库里interest表就加一条新纪录。你就执行一个mysql触发器,但是这是靠数据库解决的,你没有办法在使用其他数据库时如此简单操作。

The best solution is to modify the model by overriding the save() method of the Interest class. This method is called each time an object of class Interest is created. So open the askeet/lib/model/Interest.php file and write in the following method:

最好的方法是重写Interest类的save()方法来修改model。当Interest对象类被创建时,这个方法就被调用。打开askeet/lib/model/Interest.php,在后面写:

public function save($con = null)
{
$ret = parent::save($con);

/*先从ask_question表里取得所有数据。*/
$question = $this->getQuestion();
/*得到原来ask_question.interested_user的值。*/
$interested_users = $question->getInterestedUsers();
/*更新ask_question.interested_users的值,加1。*/
$question->setInterestedUsers($interested_users + 1);
/*保存*/
$question->save($con);

return $ret;
}

[译者:这个类继承了om/目录里对应的interest类,所以有$ret = parent::save($con);。后面大家应该都能看懂。这里简单提一下,propel-build-model命令使用一次,就更新一次om目录下的文件,就是对数据库数据的基本操作类。现在看来symfony默认规则是model目录里的文件如果存在,例如interest.php,那么当调用方法save()时,先调用这个文件(这个save()也继承了父类,正如第一行代码);如果调用getInterest()方法,而这里没有,symfony就去model/om下调用对应类里的方法,目前这是我个人的理解,请大家讨论。]

The new save() method gets the question related to the current interest, and increments its interested_users field. Then, it does the usual save(), but because a $this->save(); would end up in an infinite loop, it uses the class method parent::save() instead.

新save()方法把和interest对应的question获取,然后给interested_users字段数据加一。那么,类似一般的save()方法,但是因为$this->save()会进入死循环,所以我们用了parent::save()方法代替。

Secure the updating request with a transaction

用事物处理中的保险方法处理数据更新请求操作,让我们的数据更安全

[译者:这部分涉及到数据库事务处理和回滚,在做大项目时有参考价值。]

What would happen if the database failed between the update of the Question object and the one of the Interest object? You would end up with corrupted data. This is the same problem met in a bank when a money transfer means a first request to decrease the amount of an account, and a second request to increase another account.

如果在更新Question表和插入数据在interest表时数据库坏了怎么办?你会得到不正确的数据。同样的问题银行也碰到了,好比转帐系统第一次扣了钱,第二次给别的帐号加了钱。

If two request are highly dependent, you should secure their execution with a transaction. A transaction is the insurance that both requests will succeed, or none of them. If something wrong happens to one of the requests of a transaction, all the previously succeeded requests are cancelled, and the database returns to the state where it was before the transaction.

如果两个请求优先级都很高,你应该用一个事务处理来运行他们。一个事务处理保证两个请求都成功,或者都不成功。如果事务处理中的一个请求出错,所有先前成功的请求都被取消,数据库返回事务处理前状态。

Our save() method is a good opportunity to illustrate the implementation of transactions in symfony. Replace the code by:

我们刚才写的save()方法是个很好的例子来向你解释symfony的交易原理。把代码换成:

public function save($con = null)
{
$con = Propel::getConnection();
try
{
$con->begin();

$ret = parent::save($con);

// update interested_users in question table
$question = $this->getQuestion();
$interested_users = $question->getInterestedUsers();
$question->setInterestedUsers($interested_users + 1);
$question->save($con);

$con->commit();

return $ret;
}
catch (Exception $e)
{
$con->rollback();
throw $e;
}
}

First, the method opens a direct connection to the database through Creole. Between the ->begin() and the ->commit() declarations, the transaction ensures that all will be done or nothing. If something fails, an exception will be raised, and the database will execute a rollback to the previous state.

首先,方法通过Creole直接连接到数据库。在->begin()和->commit()之间声明,交易确保所有都执行或者不执行。如果有失败的,就抛出一个异常,数据库执行一个回滚到原来的状态。

Change the template

修改模板

Now that the ->getInterestedUsers() method of the Question object works properly, it is time to simplify the _interested_user.php fragment by replacing:

Question对象的->getInterestedUsers()方法工作正常,简单得替换一下_interested_user.php代码片断原来的代码:

<?php echo count($question->getInterests()) ?>

by

把它替换成

<?php echo $question->getInterestedUsers() ?>

Thanks to our briliant idea to use a fragment instead of leaving duplicated code in the templates, this modification only needed to me made once. If not, we would have to modify the listSuccess.php AND showSuccess.php templates, and for lazy folks like us, that would have been overwhelming.

由于我们使用了(代码)片断,而没有把重复代码放在每一个模板里,所以修改我们只需要做一次。如果没有使用(代码)片断,我们就需要修改listSuccess.php和showSuccess.php两个模板了,像我们这样的懒人程序员,这都是大家想要的。

In terms of number of requests and execution time, this should be better. You can verify it with the number of database requests indicated in the web debug toolbar, after the database icon. Notice that you can also get the detail of the SQL queries for the current page by clicking on the database icon itself:

现在的请求数量和执行时间,效率会好很多。你可以根据显示在web debug工具条显示的数据库请求数量来验证这个,就在数据库图标后。通过点击数据库图标,你可以获得当前页面执行的sql查询细节:

http://www.symfony-project.com/images/askeet/1_0/debug_before_day4.gif

http://www.symfony-project.com/images/askeet/1_0/debug_after_day4.gif

Test the validity of the modification

测试修改的效果

We’ll check that nothing is broken by requesting the show action again, but before that, run again the data import batch that we wrote yesterday:

我们再次使用show动作来确认无恙,在做这以前,运行一下昨天我们写的批处理程序导入测试数据:

$ cd /home/sfprojects/askeet/batch
$ php load_data.php

When creating the records of the Interest table, the sfPropelData object will use the overridden save() method and should properly update the related User records. So this is a good way to test the modification of the model, even if there is no CRUD interface with the Interest object built yet.

当给Interest表插入记录时,sfPropelData使用被重写的save()方法,更新了相关的User记录。所以这是个好方法来测试修改model结果,即使这里没有CRUD界面交互了(译者:这里askeet作者的意思是直接使用测试数据,而不用从网站正常访问输入数据了)

Check it by requesting the home page and the detail of the first question:

根据首页和第一个问题的细节检查:

http://askeet/frontend_dev.php/
http://askeet/frontend_dev.php/question/show/id/XX

The number of interested users didn’t change. That’s a successful move!

interested users数量没有改变。改动成功!

Same for the answers

同样的内容适用于answers

What was just done for the count($question->getInterests()) could as well be done for the count($answer->getRelevancys()). The only difference will be that an answer can have positive and negative votes by users, while a question can only be voted as ‘interesting’. Now that you understand how to modify the model, we can go fast. Here are the changes, just as a reminder. You don’t have to copy them by hand for tomorrow’s tutorial if you use the askeet SVN repository.

刚才处理count($question->getInterests())的方法同样可以用在count($answer->getRelevancys())方法上。唯一的区别在于一个答案有同意和不同意两种投票内容,问题只有感兴趣一种投票内容。现在你知道了怎样来修改model,让我们快点。提醒你有些不同。如果你使用askeet SVN,你就不需要在明天的教程里手动拷贝代码了。

Add the following columns to the answer table in the schema.xml

schema.xml增加answer表一些代码

<column name="relevancy_up" type="integer" default="0" />
<column name="relevancy_down" type="integer" default="0" />

Rebuild the model and update the database accordingly

重建model和更新数据库

$ symfony propel-build-model
$ symfony propel-build-sql
$ mysql -u youruser -p askeet < data/sql/schema.sql

Override the ->save() method of the Relevancy class in the lib/model/Relevancy.php

重写->save()方法,在lib/model/Relevancy.php

public function save($con = null)
{
$con = Propel::getConnection();
try
{
$con->begin();

$ret = parent::save();

// update relevancy in answer table
$answer = $this->getAnswer();
if ($this->getScore() == 1)
{
$answer->setRelevancyUp($answer->getRelevancyUp() + 1);
}
else
{
$answer->setRelevancyDown($answer->getRelevancyDown() + 1);
}
$answer->save($con);

$con->commit();

return $ret;
}
catch (Exception $e)
{
$con->rollback();
throw $e;
}
}

Add the two following methods to the Answer class in the model:

给lib/model的Answer类增加几个函数:

public function getRelevancyUpPercent()
{
$total = $this->getRelevancyUp() + $this->getRelevancyDown();

return $total ? sprintf(’%.0f’, $this->getRelevancyUp() * 100 / $total) : 0;
}

public function getRelevancyDownPercent()
{
$total = $this->getRelevancyUp() + $this->getRelevancyDown();

return $total ? sprintf(’%.0f’, $this->getRelevancyDown() * 100 / $total) : 0;
}

Change the part concerning the answers in question/templates/showSuccess.php by:

改变模板question/templates/showSuccess.php中的answers部分:

<div id="answers">
<?php foreach ($question->getAnswers() as $answer): ?>
<div class="answer">
<?php echo $answer->getRelevancyUpPercent() ?>% UP <?php echo $answer->getRelevancyDownPercent() ?> % DOWN
posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?>
on <?php echo format_date($answer->getCreatedAt(), 'p') ?>
<div>
<?php echo $answer->getBody() ?>
</div>
</div>
<?php endforeach; ?>
</div>

Add some test data in the fixtures

加入些测试数据

Relevancy:
rel1:
answer_id: a1_q1
user_id: fabien
score: 1

rel2:
answer_id: a1_q1
user_id: francois
score: -1

Launch the population batch

使用批处理命令

Check the question/show page

访问question/show

http://www.symfony-project.com/images/askeet/1_0/home_day4.gif

Routing

路由

Since the beginning of this tutorial, we called the URL

在开始教程前,我们输入这个URL:

http://askeet/frontend_dev.php/question/show/id/XX

The default routing rules of symfony understand this request as if you had actually requested

symfony默认的路由规则认得这个请求,就好像你输入的是:

http://askeet/frontend_dev.php?module=question&action=show&id=XX

But having a routing system opens up a lot of other possibilities. We could use the title of the questions as the URL, to be able to require the same page with:

使用路由系统带来了很多别的可能。我们可以把问题的题目写到URL里,可以这样调用同样的页面:

http://askeet/frontend_dev.php/question/what-shall-i-do-tonight-with-my-girlfriend

This would optimize the way the search engines index the pages of the website, and to make the URLs more readable.

这将优化网站内容被搜索引擎抓取索引,也让URL易读。

Create an alternate version of the title

创建一个改变的题目

First, we need a converted version of the title - a stripped title - to be used as an URL. There’s more than one way to do it, and we will choose to store this alternate title as a new column of the Question table. In the schema.xml, add the following line to the Question table:

首先,我们需要一个改变的题目——stripped title——被写到URL里。这里有不止一个办法可以做到,我们把这个被改变的题目作为Question表的一个字段处理。在schema.xml里,在Question表后面加几行:

<column name="stripped_title" type="varchar" size="255" />
<unique name="unique_stripped_title">
<unique-column name="stripped_title" />
</unique>

…and rebuild the model and update the database:

… …重建model,更新数据库:

$ symfony propel-build-model
$ symfony propel-build-sql
$ mysql -u youruser -p askeet < data/sql/schema.sql

We will soon override the setTitle() method of the Question object so that it sets the stripped title at the same time.

我们很快会重写Question对象的setTitle()方法,这样方法会重新创建stripped title。

Custom class

定制类

But before that, we will create a custom class to actually transform a title into a stripped title, since this function doesn’t really concern specifically the Question object (we will probably also use it for the Answer object).

在做这以前,我们要创建一个定制类来转换title为一个stripped title,因为这个方法真的和Question对象没有特别的关系(我们也会把这个用到Answer对象上)。

Create a new myTools.class.php file under the askeet/lib/ directory:

在askeet/lib/下建一个myTools.class.php文件,输入下面的代码:

<?php
//
class myTools
{
public static function stripText($text)
{
$text = strtolower($text);

// strip all non word chars
$text = preg_replace(’/\W/’, ‘ ‘, $text);

// replace all white space sections with a dash
$text = preg_replace(’/\ +/’, ‘-’, $text);

// trim dashes
$text = preg_replace(’/\-$/’, ”, $text);
$text = preg_replace(’/^\-/’, ”, $text);

return $text;
}
}

?>

Now open the askeet/lib/model/Question.php class file and add:

打开askeet/lib/model/Question.php并添加:

public function setTitle($v)
{
parent::setTitle($v);

$this->setStrippedTitle(myTools::stripText($v));
}

Notice that the myTools custom class doesn’t need to be declared: symfony autoloads it when needed, provided that it is located in the lib/ directory.

注意myTools定制类不需要声明:需要时,symfony自动载入,在lib/下都可以实现此功能。

[译者:symfony的lib机制很好,只需要把lib文件命名为libname.class.php即可,就可以自动调用,不必写require_once()和include_once()。]

We can now reload our data:

现在再一次载入数据:

$ symfony cc
$ php batch/load_data.php

If you want to learn more about custom class and custom helpers, read the extension chapter of the symfony book.

如果你想知道更多的关于定制类和定制helpers,看symfony book去。

Change the links to the show action

改变到show动作的链接

In the listSuccess.php template, change the line

在listSuccess.php模板中,改变原来这行

<h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>

by



<h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>

Now open the actions.class.php of the question module, and change the show action to:

现在打开question module的actions.class.php,改变show动作为:

public function executeShow()
{
$c = new Criteria();
$c->add(QuestionPeer::STRIPPED_TITLE,$this->getRequestParameter('stripped_title'));
$this->question = QuestionPeer::doSelectOne($c);

$this->forward404Unless($this->question);
}

[译者:看到这里,如果你好好看了propel官方网站上的文档,应该很简单;如果觉得不好理解,呵呵,也不要紧,让我来给你讲解一下。这句代码:
$c->add(QuestionPeer::STRIPPED_TITLE,$this->getRequestParameter('stripped_title')); 执行的就是:
select * from ask_question where stripped_title = '{$_GET['stripped_title']}

$this->question = QuestionPeer::doSelectOne($c);
执行的无非就是查询。没什么难的,去propel官方的网站上看看,别看都是英文的,但我保证那些英文单词都很简单,你一看就能看懂。

Try to display again the list of questions and to access each of them by clicking on their title:

显示问题列表并点击他们的题目进入详细页面:

http://askeet/frontend_dev.php/

The URLs correctly display the stripped title of the questions:

URL显示了question的stripped title:

http://askeet/frontend_dev.php/question/show/stripped-title/what-shall-i-do-tonight-with-my-girlfriend

Changing the routing rules

改变路由规则

But this is not exactly how we wanted them to be displayed. It is now time to edit the routing rules. Open the routing.yml configuration file (located in the askeet/apps/frontend/config/ directory) and add the following rule on top of the file:

这不完全是我们想要显示的。现在该改变路由规则了。打开routing.yml配置文件(askeet/apps/frontend/config/下),在顶部增加如下规则:

question:
url: /question/:stripped_title
param: { module: question, action: show }

In the url line, the word question is a custom text that will appear in the final URL, while the stripped_title is a parameter (it is preceded by :). They form a pattern that the symfony routing system will apply to the links to the question/show action calls - because all the links in our templates use the link_to() helper.

在URL里,question的文字作为定制文本显示在最后的URL中,而stripped_title是个参数(在冒号前)。形成symfony路由系统样式,允许question/show action调用——因为所有模板里用的链接都是link_to() helper。

It is time for the final test: Display again the homepage, click on the first question title. Not only does the first question show (proving that nothing is broken), but the address bar of your browser now displays:

到了最后测试了:再次显示首页,点第一个question的题目。不仅第一个问题显示,地址栏也显示了:

http://askeet/frontend_dev.php/question/what-shall-i-do-tonight-with-my-girlfriend

If you want to learn more about the routing feature, read the routing policy chapter of the symfony book.

如果你想知道更多的路由细节,看symfony book。

Post a Comment