symfonyのFormで確認画面を実装する方法

こんばんは。
笹亀です。

現在使用している旧MacBook(白)が最近スペックがとてもショボく見えてきましたので、
新しくパソコン(MacBookPro)を新調しました。
インテル製のSSDも入れ変えて快適生活に浸かっています。

さて、今回はsymfonyのFormで確認画面を挟んだシンプルな流れのシステムの作り方です。
いろいろと見てみた感じでちょっとわかりにくい感じでしたので、
最初にsymfonyを始める方のためにもまとめてみたいと思います。

まずは流れについてですが、
確認画面を挟む実装というのは「入力フォーム→確認画面→完了画面」の流れで処理を行うことです。入力画面で情報を入力し、確認画面で入力内容を確認し、完了画面を表示する流れをsymfonyのFormを使って作成していきます。

まずはサンプル用に作成するスキーマ情報は下記のような感じで作成しました。
 ※モデルの生成などは割愛させていただきます
  サンプルにはDoctrineを使用しています


Member:
  actAs: { Timestampable: ~ }
  tableName: member
  columns:
    name:         { type: string(255) }
    email:        { type: string(255), notnull: true }
    login_id:     { type: string(255) }
    password:     { type: string(255) }

登録フォームのオブジェクトを最低限で編集します。
 ※ここでは最低限の処理のみの実装です
lib/form/doctrine/MemberForm.class.php


<?php
/**
 * Member form.
 */
class MemberForm extends BaseMemberForm
{
  public function configure()
  {
    //フォームに使用しないフィールドをunsetする
    unset(
      $this['created_at'], $this['updated_at']
    );
  }
}

次に入力フォームのアクションを作成します。
 ※テンプレート側の処理は画像のみとします
apps/user/modules/action/action.class.php


<?php
・
・
・
  public function executeNew(sfWebRequest $request)
  {
    //Form生成
    $form = new MemberForm();
    
    //確認画面からの戻る処理
    if ($request->hasParameter('back')) {
      
      //セッションから入力情報を取得
      $values = $this->getUser()->getAttribute($form->getName());
      
      //Formにバインドする
      $form->bind($values, $request->getFiles($form->getName()));
      
    //通常新規登録処理
    } else {
      //セッションを初期化する
      $this->getUser()->setAttribute($form->getName(), null);
    }
    
    $this->form = $form;
  }

作成して実行すると下記のようなイメージになります。
 ※jobeetのデザインを使わせてもらいました。

次に確認画面のアクション(confirm)を実装します。
確認画面のアクションを実行されると入力された入力内容がPOSTされますので、
必ず入力内容をFromにbindしないといけません。
apps/user/modules/action/action.class.php


<?php
・
・
・
  public function executeConfirm(sfWebRequest $request)
  {
    //POSTではない場合は404エラーを返す
    $this->forward404Unless($request->isMethod(sfRequest::POST));
    
    //Formを生成
    $form = new MemberForm();
    
    //Formのbind処理
    $form = $this->processForm($form, $request);
    
    //エラーがある場合は新規登録テンプレート適応
    if (!$form->isValid()) {
      $this->setTemplate('new');
    }
    $this->form = $form;
    //Action側でForm
    $this->member_obj = $this->form->getObject();
    
  }

ここで注目すべきは「$this->processForm」です。
「$this->processForm」は確認画面と登録処理実行を分けて行うメソッドとして作成したものです。


確認画面の場合:
第2引数にsfWebRequestを渡し、
POSTで送られてきた入力値をFormへbindし、
送られてきた値をセッションに保存します。
 
登録処理実行の場合:
第2引数にセッションに保存した入力値のデータ配列を渡し、
Formへbindしてsaveを行います。

実際には下記のように実装します。
apps/user/modules/action/action.class.php


<?php
・
・
・
  private function processForm(sfForm $form, $request)
  {
    //確認画面用のForm処理
    if ($request instanceof sfwebRequest) {
      $values = $request->getParameter($form->getName());
      //フォームへバインド
      $form->bind($values, array());
      //Validを通したらオブジェクト更新してセッションに保存
      if ($form->isValid()) {
        $form->updateObject();
        sfContext::getInstance()->getUser()->setAttribute($form->getName(), $values);
      }
      
    //登録処理用のForm処理(確認画面からは必ずPUTにする
    } elseif(sfContext::getInstance()->getRequest()->isMethod(sfRequest::PUT)) {
      //フォームへバインド
      $form->bind($request, array());
      
      //Validを通したらオブジェクト更新して登録処理をしてセッションをクリア
      if ($form->isValid()) {
        $form->updateObject();
        $form->save();
        sfContext::getInstance()->getUser()->setAttribute($form->getName(), null);
      }
    }
    
    return $form;
  }

確認画面のテンプレートを用意します。
確認内容の場合はフォームの入力値ではなく、
セットされたモデルのオブジェクトを利用しています。

apps/user/modules/regist/templates/confirmSuccess.php


<?php use_stylesheets_for_form($form) ?>
<?php use_javascripts_for_form($form) ?>
<h1>New member confirm</h1>
<form action="<?php echo url_for('regist/create') ?>" method="post">
<input type="hidden" name="sf_method" value="put" />
<?php echo $form->renderHiddenFields() ?>
  <table>
    <tfoot>
      <tr>
        <td colspan="2">
          <input type="button" value="戻る" onClick="location.href='<?php echo url_for('regist/new') . '?back=' ?>'"> 
          <input type="submit" value="登録" />
        </td>
      </tr>
    </tfoot>
    <tbody>
    <tr>
      <th><label for="member_name"><?php echo $form['name']->renderLabel() ?></label></th>
      <td><?php echo $member_obj->get('name') ?></td>
    </tr>
    <tr>
      <th><label for="member_email"><?php echo $form['email']->renderLabel() ?></label></th>
      <td><?php echo $member_obj->get('email') ?></td>
    </tr>
    <tr>
      <th><label for="member_login_id"><?php echo $form['login_id']->renderLabel() ?></label></th>
      <td><?php echo $member_obj->get('login_id') ?></td>
    </tr>
    <tr>
      <th><label for="member_password"><?php echo $form['password']->renderLabel() ?></label></th>
      <td><?php echo $member_obj->get('password') ?></td>
    </tr>
      
    </tbody>
  </table>
</form>

実際に表示すると。。

登録処理部分の実装します。
 ※createアクションは表示するテンプレートはありません。


登録成功:完了画面へリダイレクトする。
 
登録失敗:入力フォームを表示する。

apps/user/modules/action/action.class.php


<?php
・
・
・
  public function executeCreate(sfWebRequest $request)
  {
    //PUTではない場合は404エラーを返す
    $this->forward404Unless($request->isMethod(sfRequest::PUT));
    
    //Formを生成
    $form = new MemberForm();
    //bindするパラメータを取得(CSRFも取得)
    $values = $this->getUser()->getAttribute($form->getName());
    //パラメータのチェック
    $this->forward404Unless($values);
    
    //Form処理
    $form = $this->processForm($form, $values);
    
    //エラーがなく登録が完了していたら完了画面へリダイレクト
    if ($form->isValid()) {
      $this->redirect('regist/complete');
    } else {
      $this->form = $form;
      $this->setTemplate('new');
    }
  }

最後に完了画面のアクションとテンプレートを作って完成です。

長くなってしまいましたが、コツとしてはprocessFormメソッドで処理をまとめることと、
セッションに保存する値はフォームの入力値を保存することです。
 ※間違えてもセッションにFormオブジェクトは保存しないようにしましょう。

また、processFormをstaticで使用できるようにして汎用的なものにすることで、
同じような「入力フォーム→確認ページ→完了ページ」の流れを
より簡単に作成できるようになります。

---------
※1点修正をさせていただきました
 apps/user/modules/regist/templates/confirmSuccess.php
  $member_obj = $form->getObject();

sfFormはエスケープ対象にならないので、テンプレート内でFormオブジェクトからgetObjectするとエスケープされず、XSS脆弱性になります。必ずAction側でテンプレート側に渡すようにしましょう。
 apps/user/modules/action/action.class.php
$this->member_obj = $this->form->getObject();

詳しい内容については下記をご覧くださいませ。
http://d.hatena.ne.jp/Fivestar/20100608/1275984422