通过昨天对已知技术的回顾,我们已经熟悉交互的使用。显示丰富格式的问题与列表,甚至是分页,并不足以使一个程序鲜活。而askeet概念的核心就是允许任何注册的用户询问一个新问题,而任何用户可以回答已存在的问题。现在是我们实现的时候了。
添加一个新问题
我们在第七天所创建的侧边栏已经包含一个添加新问题的链接。他链接到question/add动作,这正是我们将要开发的。
限制到注册用户的访问
首先,只有注册用户可以添加一个新问题。为了限制到question/add动作的访问,在askeet/apps/frontend/modules/question/config/目录下创建一个security.yml文件:
add:
is_secure: on
credentials: subscriber
all:
is_secure: off
当一个非注册用户试着访问一个限制动作时,Symfony将其重定向到登陆动作。这个动作必须在程序设置中进行定义,在login_module与login_action关键字之后:
all:
.actions:
login_module: user
login_action: login
关于动作访问的限制的更多信息可以在Symfony一书的安全一章进行了更多的解释。
addSuccess.php模板
question/add动作将会同时显示表单与处理表单。这就意味着要显示表单,我们只需要一个空动作。另外,如果表单验证出错,还会再次显示表单:
public function executeAdd()
{
}
public function handleErrorAdd()
{
return sfView::SUCCESS;
}
所有的动作都会输出addSuccess.php模板:
<?php use_helper('Validation') ?>
<?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
如果我们要了解更多的关于表单验证的信息,我们回到第6天,或是阅读Symfony一书的表单验证一章。
处理表单提交
现在编辑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()方法,其最大的优点在于,如果用户以后刷新详细问题页面,表单不会再次提交。另外,'back'按钮也会按所希望的方式工作。这是一条通常的规则:我们不要使用->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/interested动作中使用这个方法来替换完在同样事情的代码。现在可以进行测试了。使用一个测试用户,我们可以添加一个问题。
添加一个新答案
答案的添加将会以一种略微不同的方式来实现。在这里并没有必要使用一个表单将用户重定向到一个新页面,而是再一次到要显示答案的另一个页面。所以新的答案表单应是Ajax形式,而且新答案应立即显示在问题详细页面。
添加一个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_interested()和link_to_user_relevancy()函数中使用link_to_login()来代替link_to_function()函数。不要忘记在modules/sidebar/templatetes/defaultSuccess.php中到@add_question的链接。是的,这就是重构。
处理表单提交
虽然他仍是调用代码片段,我们在这里选择的用来处理Ajax请求的方法与我们在第八天所描述的还是有一些略微的不同。这是因为我们希望表单提交的结果实际替换表单。这就是为什么将form_remote_tag()帮助器中的更新参数指向表单本身的容器,而不是指向外层空间。_answer.php片段将会包含在答案添加动作的结果中,所以最终的结果看起来如同下面的样子:
...
<div id="answers">
<!-- Answer 1 -->
<!-- Answer 2 -->
<!-- Answer 3 -->
...
</div>
<div class="answer" id="add_answer">
<!-- The new answer -->
</div>
也许我们已经猜到了form_remote_tag() javascript是如何工作的了:他通过一个XMLHttpRequest对象将表单提交给url参数中指定的动作。动作的结果替换更新参数中指定的元素。而且,与第八天中的link_to_remote()帮助器相类似,他会激活活动指示器可见,并且依据请求的提交使其不可邮,而且在Ajax事务的结束处高亮显示更新的部分。
在这里我们要多说几名关于与一个新问题相关联的用户。我们在前面提到,答案必须链接到一个用户。如果用户已经被验证,那么他的user_id就会用于新答案。否则,就会使用anonymous,除非用户选择登陆。位于GlobalHelper.php帮助器中的link_to_login()帮助器就会激活布局中的隐藏表单。我们可以游览askeet源友来查看其代码。
answer/add动作
作为Ajax表单的url参数的@add_answer规则指向answer/add动作:
add_answer:
url: /add_anwser
param: { module: answer, action: add }
下面是动作的内容:
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;
}
$this->forward404();
}
首先,如果这个动作不是在POST模式下调用的,这就意味着是某些人在浏览器的地址栏中输入的。动作并不是为这种请类型而设置的,所以在这种情况下会返回一个404错误。
为了确定要设置为答案作者的用户,动作会检测当前用户是否已被授权。如果不是这种情况,动作就会通地UserPeer类的new::retrieveByNickname()方法使用'Anonymous Coward'用户。如果我们对这个方法还有疑惑,我们可以查看其源代码。
这样,就准备好了所有事情来创建一个新问题,并且将请求发往addSuccess.php模板。正如我们所希望的,这个模板只包含一行代码:
<?php include_partial('answer', array('answer' => $answer)) ?>
我们同时需要在frontend/modules/answer/config/view.yml中禁止这个动作的布局:
addSuccess:
has_layout: off
最后,如果用户提交一个空答案,我们并不要进行存储。所以就会略地数据处理部分,而动作并不会返回任何内容,这将简单的清除页面。我们已经在Ajax表单中完成了数据处理,但是这应将表单自身放在另一个片段中,而现在并不值得这样做。
测试
这样就完成了?正是,现在Ajax表单已经可以使用了,干净,安全。我们现在可以进行测试,显示一个问题的答案列表,并且可以添加一个新问题。这个页面并不需要刷新,而新答案显示在前面的列表的底部。这很简单,不是吗?
明天见