原文:http://www.symfony-project.com/askeet/10
回顾: 在昨天对已知技术的回顾之后,你们当中的一些人开始期盼互动性了。显示富格式的问题和清单,甚至是分页,是不足以让一个应用程序生动起来的。并且askeet的核心概念是允许注册用户提出新问题,其他用户去回答现存问题。现在是我们开始去完成它的时间?
在第七天创建的边栏已经包含了添加新问题的链接。这个链接调用还未实现的question/add动作。
首先,只有注册用户才能添加新问题。为了限制对question/add动作的访问,在askeet/apps/frontend/modules/question/config/目录中创建security.yml文件:
add: is_secure: on credentials: subscriber all: is_secure: off
当一个未登录用户尝试访问一个受控制动作,symfony将把他/她转向到登录动作。这个动作必须在应用程序setttings.yml中定义,必须位于login_module和login_action关键字之下:
all:
.actions:
login_module: user
login_action: login
更多关于动作访问控制的信息可以在symfony书中安全一章中找到。
question/add动作将被同时用在显示表格和处理表格上。这就意味着从现在开始,要显示表格,你仅仅需要一个空的动作。此外,如果在数据验证中出错,表格将会被再次显示。
public function executeAdd() { } public function handleErrorAdd() { return sfView::SUCCESS; }
这两个动作将输出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>
title和body控制器都有从同样名称的请求参数定义的默认值(表格助手的第二个参数)。为什么会是这样的?因为我们将要对表格添加一个验证文件。如果验证失败,表格会被再次显示,并且以前用户的输入仍然在请求参数中。他们可以被当作表格元素的默认值来使用。
如果验证失败,先前的输入不会丢失。这是你对用户友好应用程序的最少期待了。 但是为了实现这个目标,你需要一个表格验证文件。
在question模块中创建一个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
现在再次编辑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()); } }
记住→setTitle()方法将也设置stripped_title,并且→setBody()方也将设置html_body域。这是因为我们重载了Question.php模型类中这些方法。创建一个问题的用户将被声明对此有兴趣。这是有意阻止零兴趣问题的情况,否则会让人比较失落。 动作的结尾处包含了一个到被建立问题细节的→redirect()。较之→forward(),这样做的主要好处是如果用户过后刷新了问题细节页面,表格不会被再次递交。此外, ‘返回’按钮如同期待的那样工作。这是一条通用规则:你不应该用→forward()结束一个递交处理动作。 如果请求不是POST模式,最好的办法是动作仍然显示表单。它就表现得和之前写的空动作一摸一样,返回默认的sfView::SUCCESS调用addSuccess.php模板。 不要忘了在User模型中创建isInterestedIn()方法:
public function isInterestedIn($question) { $interest = new Interest(); $interest->setQuestion($question); $interest->setUserId($this->getId()); $interest->save(); }
就像一个小的重构一样,你可以使用user/insterested动作中这个方法置换有同样功能的代码段。 再进一步,现在就测试它。使用一个测试用户帐号,你可以添加新问题了。
问题的添加将用一个稍微不同的方法实现。这里并没有必要通过重新定向用户到一个新的表单页,然后再到另外一个页面去显示问题。因此新问题表单将采用AJAX,并且新问题将会在问题细节页立即呈现。
改写modules/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>
link_to_login()方法必须被添加到UserHelper.php助手:
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))); } }
这个函数实现了我们在其他User助手中已经看到的功能:如果用户通过验证它将显示一个到动作的链接;如果没有,连接将会被指向AJAX登录表单。因此置换在link_to_user_insterested()和link_to_user_relevancy函数中对link_to_function()的调用为link_to_login()。不要忘了在modules/sidebar/templates/defaultSuccess.php中对@add_question的链接。是的,这是重构。
尽管它仍旧牵涉到了片段(fragment),在这里用到的处理AJAX请求的方法是与第八天中讲述的那种有着轻微的不同。这是因为我们想使表单递交的结果完全取代表单的位置。这就是为什么form_remote_tag()助手的update参数指向了表单自己的容器而不是一个外部区域。_answer.php片段将被包含在问题添加动作的结果中,从而使最终的结果看起来像:
... <div id="answers"> <!-- Answer 1 --> <!-- Answer 2 --> <!-- Answer 3 --> ... </div> <div class="answer" id="add_answer"> <!-- The new answer --> </div>
你很可能已经猜到了form_remote_tage()javascript助手是怎样工作的:它通过XMLHttpRequest对象处理表单递交到在url参数指定的动作。动作的结果置换在update参数中指定的元素。并且,就像第八天的link_to_remote()助手一样,它打开和关闭活动指示器的可视性。