sfFormでjQueryのカレンダーを日本語化して使ってみる

sfFormExtraPluginをインストールする

まず、sfFormの標準のwidgetにはjQueryのカレンダー(Datepicker)を利用したwidgetがありません。
sfFormExtraPluginをインストールする必要があります。

参照: symfony 1.x legacy website

jQuery関連のライブラリを用意する

jQuery本体のライブラリをjQueryのサイトからダウンロードします。

参照: jQuery

カレンダーはjQuery UIというライブラリに含まれるので、これもダウンロードしておきます。

参照:Download Builder | jQuery UI

あと、日本語パッケージがUIのdevelopment-bundleディレクトリに含まれていますので、合計で以下の3ファイルが必要になります。

  • jquery-1.3.2.min.js
  • jquery-ui-1.7.1.custom.min.js
  • /development-bundle/ui/i18n/ui.datepicker-ja.js

また、スタイルシートもUIに含まれています。今回はsmoothnessというテーマでダウンロードしたので、以下のファイルを利用します。

jQuery関連ファイルのアップロード

jsファイル

webディレクトリにあるjsディレクトリにjQueryディレクトリを用意し以下のようなパスで配置しました。

  • アプリケーションルート/web/js/jquery/jquery-1.3.2.min.js
  • アプリケーションルート/web/js/jquery/jquery-ui-1.7.1.custom.min.js
  • アプリケーションルート/web/js/jquery/ui/i18n/ui.datepicker-ja.js
cssファイル

こちらも同じくwebディレクトリにあるcssディレクトリにsmoothnessディレクトリを用意し、以下のようなパスで配置しました。

  • アプリケーションルート/web/css/smoothness/jquery-ui-1.7.1.custom.css

日本語化したwidgetを用意する

sfFormExtraPluginにあるsfWidgetFormJQueryDateウィジットを使うのですが、どうせ日本語化に特化させるためにいろいろとオプションを指定することになるので、jpWidgetFormJQueryDateという新しいウィジットを作ってしまう事にします。

実際はプラグインで作ったのですが、ここではとりあえずライブラリとして作った場合で説明します。

jpWidgetFormJQueryDate.class.php

以下のような内容でjpWidgetFormJQueryDate.class.phpを用意します。

  • アプリケーションルート/lib/widget/jpWidgetFormJQueryDate.class.php
<?php
class jpWidgetFormJQueryDate extends sfWidgetFormJQueryDate
{
  protected function configure($options = array(), $attributes = array())
  {
    parent::configure($options, $attributes);
    $this->setOption('culture', 'ja');
    # $this->setOption('format', '%year%年%month%月%day%日'); // 古いsfWidgetFormJqueryDataを利用している場合
    $this->getOption('date_widget')->setOption('format', '%year%年%month%月%day%日');
    if ($this->getOption('config') === '{}') {
      $this->setOption('config', '{buttonText: "カレンダーで選択"}');
    }
  }
  public function getStylesheets()
  {
    return array(
                 'smoothness/jquery-ui-1.7.1.custom' => 'all'
                 );
  }
  public function getJavaScripts()
  {
    return array(
                 'jquery/jquery-1.3.2.min',
                 'jquery/jquery-ui-1.7.1.custom.min',
                 'jquery/ui/i18n/ui.datepicker-ja'
                 );
  }
}

cultureでjaを指定することで日本語化されたカレンダーを利用できます。
また、このウィジットで読み込む必要があるファイルはgetStyleSheetsとgetJavaScriptsメソッドの戻り値に配列で指定することで用意できます。ただし、自動的には読み込んでくれません。これは後述にテンプレートの部分に追記が必要。

Formクラスでwidgetを指定

そして、フォームクラスでwidgetSchemaとして指定します。

  • HogeForm.class.php
    $this->widgetSchema['expired_at'] = new jpWidgetFormJQueryDate();
CSSとJSファイルを読み込む

最後に、テンプレートで追加したスタイルシートとJSファイルを読み込むように以下のように加えます。
ウィジットに書いただけでは自動的に読み込んでくれないという不親切設計です。

  • editSuccess.php
ここでは$formにsfFormオブジェクトがアサインされている場合の例です。
<?php include_javascripts_for_form($form) ?>
<?php include_stylesheets_for_form($form) ?>
日本語化(ちょっと不自然)

これで準備は完了です。フォームを表示すると以下のようにカレンダーを利用できます。


うまくファイルが読み込まれない、日本語化されない場合は
symfony ccと、クッキーを削除するとうまくいくかも。

でも、よくみると、カレンダーのヘッダー部分の年月が惜しいことになっています。
「年」という表示が年月の間に無いために不自然な表示になってしまっています。

年月の間に「年」という文字を追加する

そこで、jQueryを使ってこのヘッダー部分に「年」という文字を追加するようにしてみます。

widgetのrenderメソッドをオーバーライドして、親クラスのレンダー結果に「年」を追加するjavascriptを書き加えます。

  • jpWidgetFormJQueryDate.class.php
<?php
...
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    return parent::render($name, $value, $attributes, $errors).<<<EOF
<script type="text/javascript">
jQuery(document).ready(function(){
  var  eleYear = ".ui-datepicker-year";
  jQuery(".ui-datepicker-trigger, div#ui-datepicker-div").click(function() {
    jQuery("span.jpYearSuffix").remove();
    jQuery(eleYear).after("<span class='jpYearSuffix'>年</span>");
  });
});
</script>
EOF
;
  }

これで、もう一度表示させてみると

月を移動しても、ちゃんと表示されています。
このように、jQueryのおかげで、コアファイルには修正を加えずにそれらしく日本語化もできましたとさ。
ただし、年をプルダウンで表示させたりすると崩れちゃいますけどね。。

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って使いやすくなる一方で学習コストがあがっていくなぁー。