Caution: You are browsing the legacy symfony 1.x part of this website.

フォームで選択機能を実装するには?

1.2
Language

フォームを表示するとき、ユーザーが選択可能なもののリストから選べるようにしたいことがよくあります。

HTMLにおいて、選択肢はselectタグによって表現されます:

selectタグ

複数の選択を受け入れられるようにmultiple属性を追加できます:

複数のselectタグ

sfWidgetFormChoice

選択はラジオボタンのリスト(単独の選択)もしくはチェックボックスのリスト(複数の選択)でも表現できます。

これらすべての実現方法を統一するために、symfony 1.2はsfWidgetFormChoiceと呼ばれる新しいウィジェットを搭載しています。 sfWidgetFormChoiceはレンダリングを別のウィジェット(レンダラウィジェット)に委譲する抽象ウィジェットです。

可能な組み合わせを描くためにシンプルな例を取り上げてみます。 プロジェクトにおいて、次のスキーマがあるとします:

データベーススキーマ

// config/schema.yml
propel:
  demo_article:
    id:           ~
    author_id:    { type: integer, foreignReference: id, foreignTable: demo_author, onDelete: cascade, onUpdate: cascade, required: true }
    status:       varchar(255)
    title:        varchar(255)
    content:      longvarchar
    published_at: timestamp
 
  demo_category:
    id:          ~
    name:        varchar(255)
 
  demo_author:
    id:          ~
    name:        varchar(255)
 
  demo_tag:
    id:          ~
    name:        varchar(255)
 
  demo_tag_article:
    tag_id:      { type: integer, primaryKey: true, foreignReference: id, foreignTable: demo_tag, onDelete: cascade, onUpdate: cascade, required: true }
    article_id:  { type: integer, primaryKey: true, foreignReference: id, foreignTable: demo_article, onDelete: cascade, onUpdate: cascade, required: true }
 
  demo_category_article:
    category_id: { type: integer, primaryKey: true, foreignReference: id, foreignTable: demo_category, onDelete: cascade, onUpdate: cascade, required: true }
    article_id:  { type: integer, primaryKey: true, foreignReference: id, foreignTable: demo_article, onDelete: cascade, onUpdate: cascade, required: true }

これらはシンプルなCMS用の古典的なスキーマです。 記事は1人の著者を持ち、たくさんのタグとカテゴリを持つことができます。 それぞれの記事は1つのステータス: publisheddraftもしくはdeletedを持つことができます。 ステータスを保存するためのテーブルは作られていないので、ステータスの値はプレーンテキストとして保存されます。

基本的なCRUDオペレーションを提供するモジュールを作ってDemoArticleモデルを遊んでみましょう:

$ php symfony propel:build-all
$ php symfony propel:generate-module frontend article DemoArticle

編集ページに移動すると、次のようなフォームが表示されます:

生のフォーム

DemoArticleモデル (lib/form/base/BaseDemoArticle.class.php)用の生成フォームクラスを見てみると、 symfonyはauthor_idに対してsfWidgetFormPropelChoiceウィジェット、 demo_category_article_listdemo_tag_article_listに対してsfWidgetFormPropelChoiceManyウィジェット を使用していることがわかります。 symfonyはスキーマの定義に基づいて使う最良のウィジェットを推測しました。

sfWidgetFormPropelChoiceはPropelオブジェクトに基づいた単独の選択を表しsfWidgetFormPropelChoiceManyはPropelオブジェクトに基づいた複数選択を表します。

フォームをカスタマイズする

フォームをカスタマイズするために最初にできることはstatusウィジェットを選択に変換することです:

ステータス用の選択があるフォーム

最初に、DemoArticlePeerモデルクラスでステータスを定義する必要があります:

// lib/model/DemoArticlePeer.php
class DemoArticlePeer extends BaseDemoArticlePeer
{
  static protected $choices = array(
    'published' => 'published',
    'draft'     => 'draft',
    'deleted'   => 'deleted'
  );
 
  static public function getStatusChoices()
  {
    return self::$choices;
  }
}

それから、ウィジェットとstatusに関連づけされたバリデータを変更するためにDemoArticleFormクラスを編集します:

// lib/form/DemoArticleForm.class.php
class DemoArticleForm extends BaseDemoArticleForm
{
  public function configure()
  {
    $this->widgetSchema['status'] = new sfWidgetFormChoice(array(
      'choices' => DemoArticlePeer::getStatusChoices()
    ));
 
    $this->validatorSchema['status'] = new sfValidatorChoice(array(
      'choices' => array_keys(DemoArticlePeer::getStatusChoices())
    ));
  }
}

sfWidgetFormChoiceselectタグで使用する選択の配列をchoicesオプションとして受け取ります。

sfValidatorChoicechoicesオプションも受け取ります。これはstatusカラムに対して有効な値です(配列DemoArticlePeer::getStatusChoices()のキー)。

選択肢で遊ぶ

ラジオボタンのリスト

sfWidgetFormChoiceウィジェットで少し遊んでみましょう! 以前のスクリーンショットで見ることができるように、ステータスはselectタグで表現できます。 ステータス用の値の数は少ないので、ステータスをラジオボタンのリストとして表示する方がベターです:

ステータス用のラジオボタンを備えているフォーム

これを実現するのはとても簡単です。 sfWidgetFormChoiceselectタグからの出力をラジオボタンのリストに変換するexpandedオプションを受け取ります:

$this->widgetSchema['status'] = new sfWidgetFormChoice(array(
  'choices'  => DemoArticlePeer::getStatusChoices(),
  'expanded' => true,
));

チェックボックスリスト

カテゴリのリストもとても小さいので、それらをチェックボックスのリストとして表示するほうがよいです:

カテゴリ用のチェックボックスリストを持つフォーム

単独選択に使用してきたexpandedオプションは複数選択のウィジェットにも使用できます。 ウィジェットは基底フォームクラスで生成され変更を必要としないのでexpandedオプションをtrueにセットするだけです:

$this->widgetSchema['demo_category_article_list']->setOption('expanded', true);

要約

次のテーブルはsfWidgetFormChoiceとレンダラーウィジェットの異なる設定を要約しています:

sfWidgetFormChoice expandedfalse expandedtrue
multiplefalse sfWidgetFormSelect sfWidgetFormSelectRadio
multipletrue sfWidgetFormSelectMany sfWidgetFormSelectCheckbox

同じスクリーンショットつきの同じテーブル:

sfWidgetFormChoice expandedfalse expandedtrue
multiplefalse 単独で展開されていない single expanded
multipletrue 複数で展開されていない multiple expanded

選択のグループを作成する

selectタグのあまり知られていない機能の1つはoptgroup機能で選択のグループを作成できる方法です:

optgroup機能

sfWidgetFormChoiceファミリーウィジェットはグループ用の組み込みのサポート機能を持ちます。 必要なのはchoicesオプションに対して配列の配列を渡すことだけです:

$choices = array(
  'Europe'  => array('France' => 'France', 'Spain' => 'Spain', 'Italy' => 'Italy'),
  'America' => array('USA' => 'USA', 'Canada' => 'Canada', 'Brazil' => 'Brazil'),
);
 
$this->widgetSchema['country'] = new sfWidgetFormChoice(array('choices' => $choices));

もちろん、ラジオボタンのリストを展開できます:

$this->widgetSchema['country'] = new sfWidgetFormChoice(array(
  'choices'  => $choices,
  'expanded' => true,
));

展開されたoptgroup機能

レンダラウィジェットによって使用されるレイアウトも使用できます:

$this->widgetSchema['country'] = new sfWidgetFormChoice(array(
  'choices'  => $choices,
  'expanded' => true,
  'renderer_options' => array('template' => '<strong>%group%</strong> %options%'),
));

カスタマイズされ展開されるoptgroup機能

そしてもちろん、multipleオプションに対しても機能します:

複数のoptgroup機能

複数で展開されているoptgroup機能

さらにJavaScript

これまでの作業は簡単でした。さらなる可能性を探るためにJavaScriptを追加してみましょう。

二重リスト

CMSが大規模で使われる場合、タグがますます増え、現在の記事に関連したタグを見つけるのがより困難になります。 このような状況に対して、二重リストウィジェットはベストの解決方法の1つです:

タグ用の二重リスト

これまでのところ、シンプルな設定(multipleexpanded)に基づいてsymfonyはベストなウィジェットを選択してきました。 しかしsfWidgetFormChoiceselectタグウィジェットをそのまま二重リストとしてレンダリングできません。

幸いにして、我々はsfWidgetFormChoiceがレンダリング作業を別のウィジェットに委譲することを知っています。 レンダリングウィジェットの変更するにはrenderer_classオプションを修正します。

sfFormExtraPluginをインストールする場合、 とても便利で興味深いウィジェットとバリデータが見つかりますが、サードパーティのライブラリに依存しているので コアに取り込まれていません。

sfWidgetFormSelectDoubleListウィジェットはそれらの1つです:

$this->widgetSchema['demo_tag_article_list']->setOption('renderer_class', 'sfWidgetFormSelectDoubleList');

ページをリフレッシュすると、ウィジェットがJavaScriptに依存しているので正しく動作しません。 ウィジェットのAPIドキュメントには適切に設定する必要がある内容が書かれています:

// apps/frontend/modules/article/templates/_form.php
<?php use_javascript('/sfFormExtraPlugin/js/double_list.js') ?>
 
<form action="<?php echo url_for('@article_update') ?>">
  <table>
    <?php echo $form ?>
 
    <!-- ... -->
  </table>
</form>

自動入力補完

まだauthor_idフィールドを遊んでいませんでした。本当にCMSにたくさんの筆者がいる場合を想像してみましょう。 ドロップダウンのselectタグでとても長い名前リストで何かを見つけるのは簡単なことではありません。 ですので、これを自動入力補完ウィジェットに変換してみましょう。

著者用の自動入力補完

著者用の自動入力補完

著者用の自動入力補完

感動的だと思いませんか?動作させるためには、以前よりも少し多くの作業をしなければなりません。

sfFormExtraPluginはJQueryライブラリに基づいた2つの自動入力ウィジェットを含みます:

  • sfWidgetFormJQueryAutocompleter: 任意の自動入力補完タスク用に使用できる
  • sfWidgetFormPropelJQueryAutocompleter: Propel用に最適化された自動入力補完機能

我々の状況において、Propel用のウィジェットを利用します:

// lib/form/DemoArticleForm.class.php
$this->widgetSchema['author_id']->setOption('renderer_class', 'sfWidgetFormPropelJQueryAutocompleter');
$this->widgetSchema['author_id']->setOption('renderer_options', array(
  'model' => 'DemoAuthor',
  'url'   => $this->getOption('url'),
));

renderer_optionsを設定することでウィジェットにいくつかのオプションを渡しました。 これらのオプションにおいて、urlurlフォームオプション($this->getOption('url'))に設定されていることにお気づきかもしれません。 フォームインスタンスを作るとき、最初のコンストラクタの最初の引数はデフォルト値で2番目の引数はオプションの配列です:

public function executeEdit($request)
{
  // ...
 
  $this->form = new DemoArticleForm($article, array('url' => $this->getController()->genUrl('article/ajax')));
 
  // ...
}

article/ajaxアクションを作る必要があります。 ウィジェットがこのアクションを呼び出すとき、いくつかのリクエストパラメータを渡します:

  • q: ユーザーが入力した文字列
  • limit: 返す項目の最大数

コードは次のとおりです:

// apps/frontend/modules/article/actions/actions.class.php
public function executeAjax($request)
{
  $this->getResponse()->setContentType('application/json');
 
  $authors = DemoAuthorPeer::retrieveForSelect($request->getParameter('q'), $request->getParameter('limit'));
 
  return $this->renderText(json_encode($authors));
}
 
// lib/model/DemoAuthorPeer.php
class DemoAuthorPeer extends BaseDemoAuthorPeer
{
  static public function retrieveForSelect($q, $limit)
  {
    $criteria = new Criteria();
    $criteria->add(DemoAuthorPeer::NAME, '%'.$q.'%', Criteria::LIKE);
    $criteria->addAscendingOrderByColumn(DemoAuthorPeer::NAME);
    $criteria->setLimit($limit);
 
    $authors = array();
    foreach (DemoAuthorPeer::doSelect($criteria) as $author)
    {
      $authors[$author->getId()] = (string) $author;
    }
 
    return $authors;
  }
}

すべてのJavaScriptウィジェットに関して、適切に動作させるためにフォームテンプレートにファイルを追加することも必要です:

// apps/frontend/modules/article/templates/_form.php
<?php use_javascript('/sfFormExtraPlugin/js/jquery.autocompleter.js') ?>
<?php use_stylesheet('/sfFormExtraPlugin/css/jquery.autocompleter.css') ?>
 
<!-- ... -->

作業は終わりました。 著者の名前を表示し、著者のidをフォームに投稿できる自動入力補完ウィジェットが手に入りました。 バリデーターのおかげで、有効なidのみが投稿されデータベースに保存されることを保証できます。

完成品のフォーム

完成品のフォームは次のとおりです。 選択に関してユーザーに質問をする方法をすべて示しています:

実現できる機能のすべて

これはたった1つのウィジェットによる柔軟性のおかげです!