[原创中文翻译]symfony askeet24:第十天,用AJAX表单更新数据。
October 5, 2007 – 2:00 am[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]
Add a new question
提交新问题
The sidebar built during day seven already contains a link to add a new question. It links to the question/add action, which is waiting to be developped.
第七天加的sidebar已经包含增加提交新问题的链接。连接到question/add动作,这是等待开发的部分。
Restrict access to registered users
限制注册会员权限
First of all, only registered users can add a new question. To restrict access to the question/add action, create a security.yml in the askeet/apps/frontend/modules/question/config/ directory:
首先,只有注册用户可以提交新问题。限制使用question/add动作,在askeet/apps/frontend/modules/question/config/下创建security.yml:
add:
is_secure: on
credentials: subscriber
#
all:
is_secure: off
When an unregistered user tries to access a restricted action, symfony redirects him/her to the login action. This action must be defined in the application settings.yml, under the login_module and login_action keys:
当未注册用户试着访问限制动作时,symfony把他/她转到login动作。这个动作必须在文件settings.yml中定义,写在login_module和login_action关键信息中:
all:
.actions:
login_module: user
login_action: login
More information about action access restriction can be found in the security chapter of the symfony book.
symfony book的安全章节详细介绍了动作使用限制。
The addSuccess.php template
写addSuccess.php模板
The question/add action will be used to both, display the form and handle the form. This means that as of now, to display the form, you only need an empty action. In addition, the form will be displayed again in case of error in the data validation:
question/add动作有两个用处,显示登录表单和处理登录表单传送过来的数据。这就意味着,显示登录表单,动作要为空。另外,万一数据验证没有通过,登录表单需要再次显示的:
public function executeAdd()
{
}
//
public function handleErrorAdd()
{
return sfView::SUCCESS;
}
Both actions will output the addSuccess.php template:
两个动作都输出到addSuccess.php模板:
<?php echo form_tag(‘@add_question’) ?>
<fieldset>
<div class=”form-row”>
<?php echo form_error(’title’) ?>
<label for=”title”>Question title:</label>
<?php echo input_tag(’title’, $sf_params->get(’title’)) ?>
</div>
<div class=”form-row”>
<?php echo form_error(’body’) ?>
<label for=”label”>Your question in details:</label>
<?php echo textarea_tag(’body’, $sf_params->get(’body’)) ?>
</div>
</fieldset>
<div class=”submit-row”>
<?php echo submit_tag(’ask it’) ?>
</div>
</form>
Both title and body controls have a default value (the second argument of the form helpers) defined from the request parameter of the same name. Why is that? Because we are going to add a validation file to the form. If the validation fails, the form is displayed again, and the previous entries of the user are still in the request parameters. They can be used as the default value of the form elements.
题目和内容控制都有默认值(表单helper的第二个参数),根据同名请求参数定义。为什么要这么做呢?因为我们要给表单加验证文件。如果验证失败,表单再次显示,先前用户输入的内容还在请求参数里。他们可以被用来当作表单的默认值。
http://www.symfony-project.com/images/askeet/1_0/add_question_error.gif
The previous entry is not lost in case of a failed form validation. That is the least you can expect of a user-friendly application.
如果验证失败,先前用户输入的内容在表单中,而没有丢失。这是你至少应该做到的基本用户体验。
But, in order to achieve that, you need a form validation file.
为了达到这个目的,你需要表单验证文件。
Form validation
表单验证
Create a validate/ directory in the question module, and add in a add.yml validation file:
在问题模块建立validate/目录,增加add.yml验证文件:
methods:
post: [title, body]
#
names:
title:
required: Yes
required_msg: You must give a title to your question
#
body:
required: Yes
required_msg: You must provide a brief context for your question
validators: bodyValidator
#
bodyValidator:
class: sfStringValidator
param:
min: 10
min_error: Please, give some more details
If you need more information about form validation, go back to day six or read the form validation chapter of the symfony book.
想知道更多表单验证知识吗?看看第六天教程或者去看看symfony book的表单验证章节。
Handle the form submission
处理表单提交数据
Now edit again the question/add action to handle the form submission:
现在编辑question/add动作来处理表单提交:
public function executeAdd()
{
if ($this->getRequest()->getMethod() == sfRequest::POST)
{
// create question
$user = $this->getUser()->getSubscriber();
//
$question = new Question();
$question->setTitle($this->getRequestParameter('title'));
$question->setBody($this->getRequestParameter('body'));
$question->setUser($user);
$question->save();
//
$user->isInterestedIn($question);
//
return $this->redirect(‘@question?stripped_title=’.$question->getStrippedTitle());
}
}
Remember that the ->setTitle() method will also set the stripped_title, and the ->setBody() method will also set the html_body field, because we overrode those methods in the Question.php model class. The user creating a question will be declared interested in it. This is intended to prevent questions with 0 interests, which would be too sad.
记得->setTitle()方法也会插入stripped_title,->setBody()方法也会给html_body字段插入内容,因为我们在Question.php模型类里重写了这些方法。会员创建问题会对自己的问题表示“感兴趣”。所以如果故意让问题有0个“感兴趣”,那样就不好了。
The end of the action contains a ->redirect() to the detail of the question created. The main advantage over a ->forward() in that if the user refreshes the question detail page afterwards, the form will not be submitted again. In addition, the ‘back’ button works as expected. That’s a general rule: You should not end a form submission handling action with a ->forward().
动作的最后包含问题细节创建时的->redirect()方法。一般都通过->forward()防止会员刷新问题页面,表单不会提交第二次。但是,如果那样的话,“后退”按扭可以导致重复提交。这是最基本的:不要用->forward()方法禁止动作处理表单提交。
The best thing is that the action still works to display the form, that is if the request is not in POST mode. It will behave exactly as the empty action written previously, returning the default sfView::SUCCESS that will launch the addSuccess.php template.
如果请求不是POST方式,动作显示登录表单。和前面写的空动作一样,默认返回sfView::SUCCESS,载入addSuccess.php模板。
Don’t forget to create the isInterestedIn() method in the User model:
别忘了给User模型创建isInterestedIn()函数:
public function isInterestedIn($question)
{
$interest = new Interest();
$interest->setQuestion($question);
$interest->setUserId($this->getId());
$interest->save();
}
As a minor refactoring, you can use this method in the user/interested action to replace the code snippet that does the same thing.
小小修饰一下,你可以用user/interested动作方法来替换掉做同样事情的代码片段。
Go ahead, test it now. Using one of the test users, you can add a question.
来吧,现在我们测试一下。使用一个测试用户,你可以提交新问题了。
Add a new answer
提交新问题
The answer addition will be implemented in a slightly different way. There is no need to redirect the user to a new page with a form, then to another page again for the answer to be displayed. So the new answer form will be in AJAX, and the new answer will appear immediately in the question detail page.
提交答案部分实现稍微有些不同。不需要把用户重定向到有表单的新页,接着答案显示在另外的页面上。所以新答案表单我们用AJAX实现,新答案会在提交后立刻在问题详细页面上显示。
Add the AJAX form
增加AJAX表单
Change the end of the modules/question/templates/showSuccess.php template by:
改变模块question/templates/showSuccess.php模板结尾部分为:
...
<div id="answers">
<?php foreach ($question->getAnswers() as $answer): ?>
<div class="answer">
<?php include_partial('answer/answer', array('answer' => $answer)) ?>
</div>
<?php endforeach; ?>
<?php echo use_helper(’User’) ?>
<div class=”answer” id=”add_answer”>
<?php echo form_remote_tag(array(
‘url’ => ‘@add_answer’,
‘update’ => array(’success’ => ‘add_answer’),
‘loading’ => “Element.show(’indicator’)”,
‘complete’ => “Element.hide(’indicator’);”.visual_effect(’highlight’, ‘add_answer’),
)) ?>
<div class=”form-row”>
<?php if ($sf_user->isAuthenticated()): ?>
<?php echo $sf_user->getNickname() ?>
<?php else: ?>
<?php echo ‘Anonymous Coward’ ?>
<?php echo link_to_login(’login’) ?>
<?php endif; ?>
</div>
<div class=”form-row”>
<label for=”label”>Your answer:</label>
<?php echo textarea_tag(’body’, $sf_params->get(’body’)) ?>
</div>
<div class=”submit-row”>
<?php echo input_hidden_tag(’question_id’, $question->getId()) ?>
<?php echo submit_tag(’answer it’) ?>
</div>
</form>
</div>
</div>
A little refactoring
来点修饰
The link_to_login() function must be added to the UserHelper.php helper:
link_to_login()方法必须被增加到UserHelper.php helper:
function link_to_login($name, $uri = null)
{
if ($uri && sfContext::getInstance()->getUser()->isAuthenticated())
{
return link_to($name, $uri);
}
else
{
return link_to_function($name, visual_effect('blind_down', 'login', array('duration' => 0.5)));
}
}
This function does something that we already saw in the other User helpers: it shows a link to an action if the user is authenticated, and if not, the link points to the AJAX login form. So replace the link_to_function() calls in the link_to_user_interested() and link_to_user_relevancy() functions by calls to link_to_login(). Don’t forget the link to @add_question in the modules/sidebar/templates/defaultSuccess.php. Yes, this is refactoring.
正如我们在其他User helper上看到的,这个方法实现了:如果用户是验证用户,就连接到动作上,如果不是,就连接到AJAX 登录表单上。所以在link_to_user_interested()和link_to_user_relevancy()上用link_to_login()调用来替换link_to_function()调用。别忘了在module/sidebar/templates/defaultSuccess.php里的链接的@add_question。小小修饰一下。
Handle the form submission
处理表单提交
Even if it still involves a fragment, the method chosen here to handle the AJAX request is slightly different from the one described during the eighth day. This is because we want the result of the form submission to actually replace the form. That’s why the update parameter of the form_remote_tag() helper points to the container of the form itself, rather than to an outer zone. The _answer.php fragment will be included in the result of the answer addition action, so that the final result can look like:
即使它也使用了碎片片断,这里处理AJAX请求的方法和第八天描述的还是有细微差别的。这是因为我们想让提交方式真正替换掉表单。这就是为什么form_remote_tag() helper更新的参数指向了表单容器自己,而不是指向外部。_answer.php代码片断将会包含在答案附加动作的结果里,所以最终结果看起来像是这样:
...
<div id="answers">
<!-- Answer 1 -->
<!-- Answer 2 -->
<!-- Answer 3 -->
...
</div>
<div class=”answer” id=”add_answer”>
<!– The new answer –>
</div>
You probably guessed how the form_remote_tag() javascript helper works: It handles the form submission to the action specified in the url argument through a XMLHttpRequest object. The result of the action replaces the element specified in the update argument. And, just like the link_to_remote() helper of day eight, it toggles the visibility of the activity indicator on and off according to the request submission, and highlights the updated part at the end of the AJAX transaction.
你可以猜测到form_remote_tag() javascript helper是怎样工作的:表单提交数据通过url传输,form_remote_tag() javascript helper通过XMLHttpRequest对象处理其中在URL里的参数。动作用新提交的数据进行更新操作。而且,就像第八天的link_to_remote() helper,它根据请求的提交切换activity indicator来决定显示还是不显示,在AJAX处理的最后部分高亮显示更新内容。
Let us add a few words about the user associated to the new answer. We previously mentioned that answers have to be linked to a user. If the user is authenticated, then his/her user_id is used for the new answer. In the other case, the anonymous user is used in place, unless the user chooses to login then. The link_to_login() helper, located in the GlobalHelper.php helper set, toggles the visibility of the hidden login form in the layout. Browse the askeet source to see its code.
让我们给用户关联的新答案加几个字。我们先前提到的答案是和用户有关系的。如果用户是注册用户,那么他/她的user_id适用于新答案。换句话说,匿名用户无法使用此功能,除非用户首先登录。link_to_login() helper,在GlobalHelper.php helper,在布局页面切换隐藏login表单。从askeet代码上可以看到。
The answer/add action
answer/add action
The @add_answer rule given as the url argument of the AJAX form points to the answer/add action:
@add_answer规则按照AJAX表单URL参数指向answer/add动作:
add_answer:
url: /add_anwser
param: { module: answer, action: add }
(In case you wonder, this configuration is to be added to the routing.yml application configuration file)
(如你所想,配置被加到了routing.yml程序配置文件)
Here is the content of the action:
这是动作的内容:
public function executeAdd()
{
if ($this->getRequest()->getMethod() == sfRequest::POST)
{
if (!$this->getRequestParameter('body'))
{
return sfView::NONE;
}
//
$question = QuestionPeer::retrieveByPk($this->getRequestParameter('question_id'));
$this->forward404Unless($question);
//
// user or anonymous coward
$user = $this->getUser()->isAuthenticated() ? $this->getUser()->getSubscriber() : UserPeer::retriveByNickname('anonymous');
//
// create answer
$this->answer = new Answer();
$this->answer->setQuestion($question);
$this->answer->setBody($this->getRequestParameter('body'));
$this->answer->setUser($user);
$this->answer->save();
//
return sfView::SUCCESS;
}
else
{//askeet 是不是漏掉了这个else
$this->forward404();
}
}
First of all, if this action is not called in POST mode, that means that someone typed its URI in a browser address bar. The action is not designed for that type of (hacker) request, so it returns a 404 error in that case.
首先,如果不是POST方式请求,这代表着有人在浏览器地址栏里输入了此URL。动作不是为这种(黑客)恶搞设计的,所以返回404错误页。
To determine the user to set as the answer’s author, the action checks if the current user is authenticated. If this is not the case, the action uses the ‘Anonymous Coward’ user, thanks to a new ::retrieveByNickname() method of the UserPeer class. Check the code if you have any doubt about what this method does.
插入答案作者到数据库中,动作检查当前用户是不是注册用户。如果不是,动作插入数据库“anonymous Coward”(匿名读者),使用新的UserPeer类的retrieveByNickname()函数。如果有疑问查看代码。
After that, everything is ready to create the new question and pass the request to the addSuccess.php template. As expected, this template contains only one line, the include_partial:
后面一切就绪,创建新问题,传递请求给addSuccess.php模板。如此,模板只包含一行代码,include_partial:
<?php include_partial('answer', array('answer' => $answer)) ?>
We also need to disable layout for this action in frontend/modules/answer/config/view.yml:
还需要关闭布局页面上的动作,打开frontend/module/answer/config/view.yml:
addSuccess:
has_layout: off
Lastly, if the user submits an empty answer, we don’t want to save it. So the data handling part is bypassed, and the action returns nothing - this will simply erase the form of the page. We could have done error handling in this AJAX form, but it would imply putting the form itself in another fragment. That is not worth the effort for now.
最后,如果用户提交了空内容的答案,我们不想把这种答案保存到数据库里。那么数据处理就忽略,action返回空——简单得清空页面表单。我们可以在AJAX表单里做错误处理,也可以把这个放到代码片断里。现在还不值得这么干。
Test it
测试
Is that all? Yes, the AJAX form is ready to be used, clean and safe. Test it by displaying the list of answers to a question, and by adding a new answer to it. The page doesn’t need a refresh, and the new answer appears at the bottom of the list of previous ones. That was simple, wasn’t it?
这就够了吗?是的,AJAX可以用了,简洁而且安全。通过显示问题详细内容页面的答案列表来测试一下,提交个新答案测试一下。页面不需要刷新,新答案在已发布答案列表底部出现。够简单的,对不?