通過昨天對已知技術的回顧,我們已經熟悉互動的使用。顯示豐富格式的問題與列表,甚至是分頁,並不足以使一個程式鮮活。而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表單已經可以使用了,乾淨,安全。我們現在可以進行測試,顯示一個問題的答案列表,並且可以添加一個新問題。這個頁面並不需要重新整理,而新答案顯示在前面的列表的底部。這很簡單,不是嗎?
明天見