add to hatena hatena.comment (0) add to del.icio.us (0) add to livedoor.clip (0) add to Yahoo!Bookmark (0) Total: 0

DAY10: AJAXでデータ操作

質問の投稿

新しい質問の投稿をAJAXを使ったフォームで実装していきます。
ログインフォームをAJAXで実装しているので基本は同じです。
symfony1.0との違いはやはりsfFormを利用した実装です。

今日の完成形は以下のようになります。


ログインしているユーザーだけに許可

質問はログインしているユーザーだけに許可することにするので質問投稿フォームであるquestion/addに制限を加えてあります。

./apps/frontend/modules/question/config/security.yml

CSS:
  1. add:
  2.   is_secure: on
  3.   credentials: subscriber
  4. all:
  5.   is_secure: off

ログインしていないユーザーがアクセスしてきた場合はログインフォームを表示させたいので、
setting.ymlにて遷移先を指定しておきます。

./apps/frontend/config/setting.yml

CSS:
  1. all:
  2.   .actions:
  3.     login_module:           user
  4.     login_action:           login

質問投稿フォームの作成

ログインフォームの実装と同じですね。sfFormを使って質問の投稿フォームを作成していきます。
まずはQuestionの基底モデルを作成しておきます。

./lib/form/doctrine/QuestionForm.class.php

PHP:
  1. ...
  2.   public function configure()
  3.   {
  4.     // unset fields
  5.     $unset_fields = array(
  6.                           'created_at',
  7.                           'updated_at',
  8.                           );
  9.     foreach ($unset_fields as $value) {
  10.       $this->offsetUnset($value);
  11.     }
  12.     //////////////////////////////////////////////////
  13.     // title
  14.     $key = 'title';
  15.     $this->widgetSchema[$key] = new sfWidgetFormInput();
  16.     $this->widgetSchema->setLabel($key, '件名');
  17.     $this->validatorSchema[$key] = new sfValidatorString();
  18.     $_max = 100;
  19.     $this->validatorSchema[$key]->setMessage('max_length', sprintf('文字数は%d文字以内で入力してください', $_max));
  20.     $this->validatorSchema[$key]->setOption('max_length', $_max);
  21.     $this->validatorSchema[$key]->setOption('required', true);
  22.     $this->validatorSchema[$key]->setMessage('required', '件名は必須です');
  23.     //////////////////////////////////////////////////
  24.     // body
  25.     $key = 'body';
  26.     $this->widgetSchema[$key] = new sfWidgetFormTextarea();
  27.     $this->widgetSchema->setLabel($key, '質問内容');
  28.     $this->validatorSchema[$key] = new sfValidatorString();
  29.     $_max = 1000;
  30.     $this->validatorSchema[$key]->setMessage('max_length', sprintf('文字数は%d文字以内で入力してください', $_max));
  31.     $this->validatorSchema[$key]->setOption('max_length', $_max);
  32.     $this->validatorSchema[$key]->setOption('required', true);
  33.     $this->validatorSchema[$key]->setMessage('required', '質問内容は必須です');
  34.   }

これを親とするPostQuestionForm.class.phpを作成します。
フォームから受け取る値ではなく、sfUserインスタンスに保持しているユーザーIDをアサインする必要があるので、
保存する前に呼び出されるupdateObjectメソッドを使ってuser_idをアサインしています。

./lib/form/doctrine/PostQuestionForm.class.php

PHP:
  1. class PostQuestionForm extends QuestionForm
  2. {
  3.   public function configure()
  4.   {
  5.     parent::configure();
  6.     $unset_fields = array(
  7.                           'interested_users',
  8.                           'user_id',
  9.                           );
  10.     foreach ($unset_fields as $value) {
  11.       $this->offsetUnset($value);
  12.     }
  13.   }
  14.   public function updateObject($values = null)
  15.   {
  16.     $object = parent::updateObject($values);
  17.     $object->setUserId(sfContext::getInstance()->getUser()->getSubscriberId());
  18.     return $object;
  19.   }
  20. }

あとはaddアクションとaddSuccessテンプレートを用意すれば完了です。
sfFormでORMのsaveメソッドを叩くには$this->form->save()を利用します。
また保存したQuestionインスタンスが戻ってくるのでそれを利用して投稿したページへリダイレクトしています。

./apps/frontend/modules/question/actions/actions.class.php

PHP:
  1. ...
  2.   public function executeAdd($request)
  3.   {
  4.     // get sfForm object
  5.     $this->form = new PostQuestionForm();
  6.     // check Request Method
  7.     if (!$request->isMethod('post')) {
  8.       return sfView::SUCCESS;
  9.     }
  10.     $this->form->bind($request->getParameter('question'));
  11.     if ($this->form->isValid()) {
  12.       $question = $this->form->save();
  13.       $this->redirect('question/show?id=' . $question->getId());
  14.     }
  15.     return sfView::SUCCESS;
  16.   }

./apps/frontend/modules/question/templates/addSuccess.php

PHP:
  1. <form action="<?php echo url_for('@add_question') ?>" method="POST">
  2. <table>
  3. <?php echo $form ?>
  4. <tr>
  5.   <td colspan="2" style="text-align: right"><input type='submit' value="投稿する"></td>
  6. </tr>
  7. </table>
  8. </form>

回答を追加できるようにする

本家と同様におこなっていきます。
AJAXでの回答フォームを追加していきます。

PostAnserFormというフォームインスタンスを作成するので、以前行ったのと同様で
コンポーネントを利用するようにします。

./apps/frontend/modules/question/tempaltes/showSuccess.php

PHP:
  1. <?php include_component('answer', 'add', array('question_id' => $question->getId())) ?>

コンポーネントで質問ID(question_id)を用いて新しいAnswerインスタンスを作り
それをインスタンス作成時に渡しています。
こうすることで、初期値をsfFormが自動的におこなってくれます。

./apps/frontend/modules/answer/actions/components.class.php

PHP:
  1. <?php
  2. class answerComponents extends sfComponents
  3. {
  4.   public function executeAdd(sfWebRequest $request)
  5.   {
  6.       $answer = new Answer();
  7.       $answer->setQuestionId($this->question_id);
  8.       $this->form = new PostAnswerForm($answer);
  9.    }
  10. }

テンプレートを用意しておきます。
そして、サブミットをAJAXにて処理するためform_remote_tagを使用しています。

./apps/frontend/modules/asnwer/templates/_add.php

PHP:
  1. <div class="answer" id="add_answer">
  2.  
  3. <?php echo use_helper('User', 'JavaScript') ?>
  4.  
  5. <div class="answer" id="add_answer">
  6.   <?php echo form_remote_tag(array(
  7.     'url'      => '@add_answer',
  8.     'update'   => array('success' => 'add_answer'),
  9.     'loading'  => "Element.show('indicator')",
  10.     'complete' => "Element.hide('indicator');".visual_effect('highlight', 'add_answer'),
  11.   )) ?>
  12.  
  13. <table>
  14. <?php echo $form ?>
  15. <tr>
  16.  <td colspan="2"><input type="submit" value="回答する"></td>
  17. </tr>
  18. </table>
  19. </form>
  20. </div>

ログインへのリンクのヘルパーを追加

本家のチュートリアルではここでいきなりリファクタリングが始まります。
やりたいことはlink_to_loginとしてGlobalヘルパーで共通化してしまうということです。

./apps/frontned/lib/helper/UserHelper.php

PHP:
  1. use_helper('Javascript', 'Global');
  2. ....
  3.     return link_to_login('interested?');
  4.     //    return link_to_function('interested?', visual_effect('blind_down', 'login', array('duration' => 0.5)));

Globalヘルパーにlink_to_functionを実装します。

./apps/frontend/lib/helper/GrobalHelper.php

PHP:
  1. function link_to_login($name, $uri = null)
  2. {
  3.   if ($uri && sfContext::getInstance()->getUser()->isAuthenticated())
  4.   {
  5.     return link_to($name, $uri);
  6.   }
  7.   else
  8.   {
  9.     return link_to_function($name, visual_effect('blind_down', 'login', array('duration' => 0.5)));
  10.   }
  11. }

これで、サイドバーの`ask a new question`をlink_to_loginに書き換えることができます。

./apps/frontend/modules/sidebar/templates/_default.php

PHP:
  1. <?php use_helper('Global') ?>
  2. <?php echo link_to_login('ask a new question', '@add_question') ?>
  3. ....

sfFormの実装

ようやく、sfFormの用意です。いままでと同じなのですね。
まずは親クラスを用意しておきます。

./lib/form/doctrine/AnswerForm.class.php

PHP:
  1. ...
  2.   public function configure()
  3.   {
  4.     // unset fields
  5.     $unset_fields = array(
  6.                           'created_at',
  7.                           );
  8.     foreach ($unset_fields as $value) {
  9.       $this->offsetUnset($value);
  10.     }
  11.     //////////////////////////////////////////////////
  12.     // body
  13.     $key = 'body';
  14.     $this->widgetSchema[$key] = new sfWidgetFormTextarea();
  15.     $this->widgetSchema->setLabel($key, '回答内容');
  16.     $this->validatorSchema[$key] = new sfValidatorString();
  17.     $_max = 1000;
  18.     $this->validatorSchema[$key]->setMessage('max_length', sprintf('文字数は%d文字以内で入力してください', $_max));
  19.     $this->validatorSchema[$key]->setOption('max_length', $_max);
  20.     $this->validatorSchema[$key]->setOption('required', true);
  21.     $this->validatorSchema[$key]->setMessage('required', '回答内容は必須です');
  22.   }

そして、回答投稿用のフォームクラスを作成します。
question_idは
./lib/form/doctrine/PostAnswerForm.class.php

PHP:
  1. <?php
  2. class PostAnswerForm extends AnswerForm
  3. {
  4.   public function configure()
  5.   {
  6.     parent::configure();
  7.     $unset_fields = array(
  8.                           'user_id',
  9.                           'relevancy_up',
  10.                           'relevancy_down',
  11.                           );
  12.     foreach ($unset_fields as $value) {
  13.       $this->offsetUnset($value);
  14.     }
  15.     //////////////////////////////////////////////////
  16.     // question_id
  17.     $key = 'question_id';
  18.     $this->widgetSchema[$key] = new sfWidgetFormInputHidden();
  19.   }
  20.   public function updateObject($values = null)
  21.   {
  22.     $object = parent::updateObject($values);
  23.     $object->setUserId(sfContext::getInstance()->getUser()->getSubscriberId());
  24.     return $object;
  25.   }
  26. }

なにかと大変ですが、テンプレートで指定しているルーティングルールを追加しておきます。
また、本家のリポジトリを覗くと色々と定義がされているのであわせておきました。

./apps/frontend/config/routing.yml

CSS:
  1. add_answer:
  2.   url:   /add_answer
  3.   param: { module: answer, action: add }

最後はaddアクションの準備です。

./apps/frontend/moules/answer/actions/actions.class.php

PHP:
  1. ...
  2.   public function executeAdd(sfWebRequest $request)
  3.   {
  4.     $this->form = new PostAnswerForm();
  5.     $this->form->bind($request->getParameter('answer'));
  6.     if ($this->form->isValid()) {
  7.       $this->answer = $this->form->save();
  8.       return sfView::SUCCESS;
  9.     }
  10.     return sfView::ERROR;
  11.   }

本家と異なる点は処理が正しく行われたかどうかをsfView::ERRORとSUCCESSで返している点です。
これで、それぞれのテンプレートはaddError.php、addSuccess.phpになります。

このメリットはテンプレートをみればどういったステータスなのかが説明がなくてもわかるということですが、
その反面テンプレートの数が増えてしまうというデメリットもあります。
なので、sfView:SUCCESSを返すようにし、テンプレート内でifで分岐するという方法のほうが解りやすいかもしれませんね。

さて、今回はテンプレートを分けましのたのでそれぞれを実装していきます。
まずはエラーがあった場合のテンプレートです。
もう一度フォームを表示させる必要があるので、さきほど作成したコンポーネントを呼ぶようにしています。
さきほどはquestion_idを渡していましたが、ここでは$formそのものを渡すようにしておきます。

./apps/frontend/modules/answer/templates/addError.php

PHP:
  1. <?php include_component('answer', 'add', array('form' => $form)) ?>

そして、コンポーネントアクションで$formが渡されてきたときはそれをそのまま利用するように書き換えておきます。
./apps/frontend/modules/answer/actions/components.class.php

PHP:
  1. ...
  2.   public function executeAdd(sfWebRequest $request)
  3.   {
  4.     if (!$this->form) {
  5.       $answer = new Answer();
  6.       $answer->setQuestionId($this->question_id);
  7.       $this->form = new PostAnswerForm($answer);
  8.     }
  9.   }

これでエラーがあったときは既存のコンポーネントを再利用して入力フォームを表示します。

次に正常終了時の処理です。
投稿された回答を表示するので、回答1つずつをパーシャルとして切り分けておきます。

./apps/frontend/modules/answer/templates/_answer.php

PHP:
  1. <?php use_helper('Date') ?>
  2.  
  3. <div class="vote_block" id="vote_<?php echo $answer->getId() ?>">
  4.   <?php echo include_partial('answer/vote_user', array('answer' => $answer)) ?>
  5. </div>
  6. posted by <?php echo $answer->getUser() ?>
  7. on <?php echo format_date($answer->getCreatedAt(), 'p') ?>
  8. <div>
  9.   <?php echo $answer->getBody() ?>
  10. </div>

そして、これを呼び出すテンプレートにinclude_partialを記述します。
まずは、回答投稿が正常に終了した場合のテンプレート

./apps/frontend/modules/answer/templates/addSuccess.php

PHP:
  1. <?php include_partial('answer', array('answer' => $answer)) ?>

質問表示のページにも記述しておきます。
こちらはquestionモジュール内のテンプレートのため、パーシャルの呼び出しにanswer/answerとモジュール名を指定しています。
./apps/frontend/moduels/question/tempaltes/showSuccess.php

PHP:
  1. <?php foreach ($question->getAnswer() as $answer): ?>
  2.   <div>
  3.      <?php include_partial('answer/answer', array('answer' => $answer)) ?>
  4.   </div>
  5. <?php endforeach; ?>

本家チュートリアルのリポジトリでは本文部分が質問と同じくMarkdown対応になってたりするんですが、
まずはこれで回答の実装を終える事にします。

また明日

symfony1.2のフォームを使う事に徐々になれてきました。
そして、sfFormの書き方になれればアクション、コンポーネントはかなりスッキリして気持ちよいものです。

ここまでのソースは以下のリポジトリからチェックアウトすることができます。
http://svn.1ms.jp/public/symfony/askeet12/tags/release_day_10

関連するその他の記事

Comments

Leave a Reply