sfObjectRouteで確認画面を作ってみる

[追記] 4/9 sfObjectRouteCollectionを使った場合も追加

sfObjectRouteとは?

アシアルさんのブログが一番わかりやすいので、そちらを最初に読むとよくわかります。
参照: symfony 1.2のルーティングまとめ - アシアルブログ

sfObjectRouteを使うメリットは?

アクションの記述が減ります。ルーティングのルールに従って処理されるからです。
データベースからidで値を持ってくる処理は、ルーティングの設定さえ行えば、アクションにはモデル取得のための記述が

$job = $this->getRoute()->getObject();

だけになります。

sfObjectRouteを使った場合のデメリットは?

今回のお題のような確認画面を新しく作りたいというような場合にどうやっていいか悩む。
そして、場合によっては、ルールを超えるために複雑なコーディングになるかもしれないし、場合によってはsfObjectRouteを使わない方が良い場合もある。

確認画面を作ってみる

今回はJobeetのJobeetJobというモデルに対しての編集、更新処理を行う部分で実装してみます。

通常のsfObjectRouteに確認画面のために追加する -ルーティング設定-

symfonyでは以下のような編集画面(job_edit)と更新処理(job_update)を想定しています。

# apps/frontend/config/routing.yml
job_edit:
  url:     /job/:id/edit.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: edit, sf_format: html }
  requirements: { sf_method: get }
 
job_update:
  url:     /job/:id.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: update, sf_format: html }
  requirements: { sf_method: put }

ここで、確認画面は以下のように考えてみました。

  • 確認画面はjob_updateに対して、putでなくpostする
  • 入力画面に戻る処理のためにjpb_reeditというルーティングを用意し、このルーティングにpostする

そして、以下のように修正してみました。

# apps/frontend/config/routing.yml
job_edit:
  url:     /job/:id/edit.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: edit, sf_format: html }
  requirements: { sf_method: get }

job_reedit:
  url:     /job/:id/edit.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: edit, sf_format: html }
  requirements: { sf_method: post }

job_update:
  url:     /job/:id.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: update, sf_format: html }
  requirements: { sf_method: [post, put] }

また、このルーティングの書き方が鬱陶しいなぁと言う場合は、sfObjectRouteCollectionを使うとよいです。
上記のような設定を以下のように短く書き換える事ができます。

# apps/frontend/config/routing.yml
job:
  class: sfDoctrineRouteCollection
  options:
    model: JobeetJob
    actions: [edit, update]
    collection_actions: []
    object_actions:
      reedit: post
      update: [post, put]

actionsで使用するアクションを定義し、object_actionsで追加もしくは変更するアクション名とそのsf_methodを定義します。
コレクションを使わない先ほどのrouting.ymlとは微妙に異なります。
アクションとしてreeditアクションそのものを定義しているので、reeditアクションも作成しなくてはなりません。
それは次のアクションの改修部分で言及します。

通常のsfObjectRouteに確認画面のために追加する -アクション-

アクションでsf_methodは$request->isMethod('post')のようにして確認できます。これを利用して、処理を分岐していきます。
また、確認画面で利用するテンプレートは編集画面と共通でeditとし、確認画面用のhidden作成やfreezeの機能などは以前取り上げたネタの流用です。

参照: sfFormで確認画面を作るためのhidden作成 - ぷぎがぽぎ

  • actions.class.php
<?php
class jobActions extends sfActions
{
  public function executeUpdate(sfWebRequest $request)
  {
    $this->setTemplate('edit');
    $this->form = new JobeetJobForm($this->getRoute()->getObject());
    $this->form->bind($request->getParameter($this->form->getName()));
    if ($this->form->isValid()) {
      // method == post => confirm
      if ($request->isMethod('post')) {
        $this->form->freeze(); // widgetを全てhiddenに変換する
        return sfView::SUCCESS;
      }
      // method == put => update
      if ($request->isMethod('put')) {
        $job = $this->form->save();
        $this->redirect($this->generateUrl('job_edit', $job));
      }
    }
  }
  public function executeEdit(sfWebRequest $request)
  {
    $this->form = new JobeetJobForm($this->getRoute()->getObject());
    // method == post => return from confirm
    if ($request->isMethod('post')) $this->form->bind($request->getParameter($this->form->getName()));
  }

sf_methodで確認画面なのか、編集画面に戻ってきた遷移なのかを判断しています。
sfObjectRouteを利用しているおかげもあり、かなりすっきりです。

sf_methodを用いない場合では、hiddenでmode=confirmなどというようなやり方をしていたりしますが、このsf_methodを用いた方法のほうがすっきりしますね。

また、sfObjectRouteCollectionでrouting.ymlを指定した場合はreeditアクションそのものも定義しなくてはなりません。
実際の処理は入力画面に戻るだけですので、以下のようなシンプルなメソッドになります。forwardする方法もありますが、余計なフィルタ処理などを通したくないので直接editアクションのメソッドを呼び出してみました。

<?php
...
  public function executeReedit(sfWebRequest $request)
  {
    $this->setTemplate('edit');
    $this->executeEdit($request);
  }
通常のsfObjectRouteに確認画面のために追加する -テンプレート-

テンプレートはeditSuccess.phpで共通なので確認画面かどうかの判断が必要になります。
sf_methodを用いる方法と自前拡張のisFreezeを用いる方法の2つがあります。

  • editSuccess.php
// その1 sf_methodで確認(アクションがupdateでsf_methodがpostの場合は確認画面)
<?php if ($sf_request->isMethod('post') && $sf_request->getParameter('action') === "update"): ?>
確認画面ですよ
<?php else: ?>
編集画面ですよ
<?php endif ?>

// その2 isFreezeで確認
<?php if ($form->isFreeze): ?>
確認画面ですよ
<?php else: ?>
編集画面ですよ
<?php endif ?>

とりあえずここでは、sf_methodを使うパターン説明を続けます。

あと、入力画面にもどったりするためにそれぞれフォームタグを用意します。ちょっと不細工なんですが、分かりやすいかと思いますので。。

  • editSuccess.php
<?php if ($sf_request->isMethod('post') && $sf_request->getParameter('action') === "update"): ?>

....[確認画面表示]....
  <!-- 入力画面に戻る -->
  <form method="post" action="<?php echo url_for('job_reedit', $form->getObject()) ?>">
  <?php echo $form ?>
  <input type="submit" name="submit" value="入力画面に戻る">
  </form>

  <!-- 更新処理を実行する -->
  <?php echo form_tag_for($form, '@job') ?>
  <?php echo $form ?>
  <input type="submit" name="submit" value="実行する">
  </form>

<?php else: ?>

  <!-- 編集画面表示 -->
  <?php echo form_tag_for($form, '@job', array('method' => 'post')) ?>
  <table>
  <?php echo $form ?>
  </table>
  <input type="submit" name="submit" value="確認する">
  </form>

<?php endif ?>

確認画面が無い場合でsfObjectRouteを使う場合は、form_tag_forヘルパーを使えば良いのですが、
今回のように、job_reeditというような自前のルーティングを指定したい場合には使えないみたいです。
なので、入力画面に戻るformタグはurl_forを使っています。ただし、getObjectメソッドが使えるのでid=XXXのような指定は不要です。

また、確認画面に進む場合は、sf_methodをpostに変更しなければならないので、第3引数でmethodというキーで'post'を指定しておきます。

RESTについてはきちんと理解できていないかもしれないので、突っ込みどころ満載かもしれませんが、可読性もほどほどよいですし、なによりアクションにDoctrineやPropelの存在を感じさせずにコードが書けるところが良いですね。

(おまけ)sfObjectRouteでモデルから値をとってくる処理を変更したい場合

ちなみに、モデルからデータをとってくるときに、idだけではなく、セッションに保持している値(ログインIDなど)を用いてモデルから値を取得したい場合もあります。この場合はrouting設定でoptionsでmethodを指定すればOKです。

たとえば、DoctrineでmyMethodを指定する場合は以下のようにします。

# apps/frontend/config/routing.yml
job_edit:
  url:     /job/:id/edit.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object, method: myMethod }
  param:   { module: job, action: edit, sf_format: html }
  requirements: { sf_method: get }

あとは、モデル(JobeetJobTable.class.php)にmyMethodを作成します。このとき、パラメータは配列で渡されます。

<?php
....
public function myMethod($params)
{
  // パラメータは配列で取得
  $id = $params['id'];
  // 新しい処理などを追加
  $user_name = sfContext::getInstance()->getUser()->getUserName();
  ....
}

また、sfObjectRouteCollectionでrouting.ymlを書いている場合はmodel_methodsにobjectというキーでメソッドを登録します。

# apps/frontend/config/routing.yml
job:
  class: sfDoctrineRouteCollection
  options:
    model: JobeetJob
    actions: [edit, update]
    collection_actions: []
    object_actions:
      reedit: post
      update: [post, put]
    model_methods:
      object: myMethod

初めてsfObjectRouteを使ってみたのですが、実践でも使える機能だと感じました。
理解するのに時間もかかってしまいますが、やっぱりアクションに記述するコードがすっきりするのが良いですね。
にしても、symfonyって使いやすくなる一方で学習コストがあがっていくなぁー。