フォームは、隠し入力、テキスト入力、選択ボックス、およびチェックボックスのようなフィールドで構成されます。この章ではsymfonyのフォームフレームワークを利用してフォームを作成し、フォームフィールドを管理する方法を説明します。
この章を読むにあたり、symfony 1.1が必要です。また、読み進めるにはプロジェクト、およびfrontend
アプリケーションを作成する必要があります。symfonyプロジェクトの作成方法に関する詳細な情報は、入門の手引きを参照してください。
始める前に
問い合わせフォームをsymfonyアプリケーションに追加することから始めます。
図1-1は、ユーザーがメッセージを送りたい場合に表示される問い合わせフォームです。
図1-1 - 問い合わせフォーム
このフォームに3つのフィールドを作成します: ユーザーの名前、ユーザーのEメール、および送信するメッセージです。この練習では、図1-2のようにフォームに投稿された情報を単に表示するのが目的です。
図1-2 - お礼ページ
図1-3 - アプリケーションとユーザーの間のインタラクション
ウィジェット
sfFormクラスとsfWidgetクラス
ユーザーはフォームを構成するフィールドに情報を入力します。symfonyでは、フォームはsfForm
クラスを継承するオブジェクトです。この例では、sfForm
クラスを継承するContactForm
クラスを作成します。
note
sfForm
はすべてのフォームの基底クラスで、フォームの設定とライフサイクルの管理を簡単にします。
configure()
メソッドにウィジェットを追加するコードを記述することからフォームの設定を始めましょう。
ウィジェットはフォームフィールドを表します。この例のフォームでは、3つのフィールド: name
, email
、およびmessage
を表す3つのウィジェットを追加する必要があります。リスト1-1に、ContactForm
クラスの最初の実装を示します。
リスト1-1 - 3つのフィールドがあるフォームのContactForm
クラス
// lib/form/ContactForm.class.php class ContactForm extends sfForm { public function configure() { $this->setWidgets(array( 'name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), 'message' => new sfWidgetFormTextarea(), )); } }
ウィジェットはconfigure()
メソッド内で定義します。このメソッドは、sfForm
クラスのコンストラクタによって自動的に呼び出されます。
setWidgets()
メソッドは、フォームで使用するウィジェットを定義するために使われます。setWidgets()
メソッドは、キーがフィールド名で値がウィジェットのオブジェクトである連想配列を受け取ります。それぞれのウィジェットはsfWidget
クラスを継承するオブジェクトです。この例では2種類のウィジェットを使いました:
sfWidgetFormInput
: このウィジェットはinput
フィールドを表しますsfWidgetFormTextarea
: このウィジェットはtextarea
フィールドを表します
note
慣習として、フォームクラスをlib/form/
ディレクトリに保存します。これらのフォームクラスは、symfonyのオートローディングメカニズムで管理される任意のディレクトリに保存できます。後の章で説明するように、モデルオブジェクトからフォームを生成するためにsymfonyはlib/form/
ディレクトリを利用します。
フォームを表示する
フォームを使う準備ができました。フォームを表示するためのsymfonyモジュールを作成しましょう:
$ cd ~/PATH/TO/THE/PROJECT $ php symfony generate:module frontend contact
contact
モジュールで、フォームのインスタンスをテンプレートに渡すよう、リスト1-2のようにindex
アクションを修正しましょう。
リスト1-2 - contact
モジュールのアクションクラス
// apps/frontend/modules/contact/actions/actions.class.php class contactActions extends sfActions { public function executeIndex() { $this->form = new ContactForm(); } }
フォームがインスタンス化される際、リスト1-1で定義したconfigure()
メソッドは自動的に呼び出されます。
アクションの修正と合わせて、フォームを表示するためのテンプレートをリスト1-3のように作成する必要があります。
リスト1-3 - フォームを表示するテンプレート
// apps/frontend/modules/contact/templates/indexSuccess.php <form action="<?php echo url_for('contact/submit') ?>" method="POST"> <table> <?php echo $form ?> <tr> <td colspan="2"> <input type="submit" /> </td> </tr> </table> </form>
symfonyのフォームは、情報をユーザーに表示するウィジェットのみを取り扱います。indexSuccess
テンプレートにおいて、<?php echo $form ?>
の行は3つのフィールドを表示するのみです。form
タグや投稿ボタンなどの別の要素は、開発者が追加する必要があります。これは最初自明ではないかもしれませんが、フォームの埋め込みを行う場合に便利で簡単な方法であることを、後に説明します。
プロトタイプを作成する場合や、フォームを定義する場合に、<?php echo $form ?>
構文はとても便利です。これによって、開発者は視覚的な面に悩むことなく、ビジネスロジックに集中できるようになります。第3章では、テンプレートとフォームレイアウトをパーソナライズする方法を説明します。
note
<?php echo $form ?>
を利用してオブジェクトを表示するとき、PHPエンジンは実際には$form
オブジェクトのテキスト表現を表示します。オブジェクトを文字列に変換するために、PHPは__toString()
マジックメソッドを実行しようとします。それぞれのウィジェットには、オブジェクトをHTMLコードに変換するためにこのマジックメソッドが実装されています。<?php echo $form ?>
を呼び出すことは、<?php echo $form->__toString() ?>
を呼び出すことと同等です。
ブラウザでフォームを表示してみます(図1-4)。contact/index
アクションのアドレス(/frontend_dev.php/contact
)を入力して、結果をチェックしましょう。
図1-4 - 生成された問い合わせフォーム
リスト1-4は、テンプレートによって生成されたコードです。
<form action="/frontend_dev.php/contact/submit" method="POST"> <table> <!-- Beginning of generated code by <?php echo $form ?> --> <tr> <th><label for="name">Name</label></th> <td><input type="text" name="name" id="name" /></td> </tr> <tr> <th><label for="email">Email</label></th> <td><input type="text" name="email" id="email" /></td> </tr> <tr> <th><label for="message">Message</label></th> <td><textarea rows="4" cols="30" name="message" id="message"></textarea></td> </tr> <!-- End of generated code by <?php echo $form ?> --> <tr> <td colspan="2"> <input type="submit" /> </td> </tr> </table> </form>
フォームオブジェクトがHTMLテーブルの<tr>
行3つとして表示されていることが分かります。このため、フォームを<table>
タグで囲んでおく必要があります。それぞれの行には、<label>
タグ、および<input>
や<textarea>
といったフォームタグが含まれます。
ラベル
それぞれのフィールドのラベルは自動的に生成されます。デフォルトでは、ラベルは次の2つのルールに従って、フィールド名から変換されます: 先頭は大文字に、アンダースコアはスペースに置き換えられます。 例:
$this->setWidgets(array( 'first_name' => new sfWidgetFormInput(), // 生成されたラベル: "First name" 'last_name' => new sfWidgetFormInput(), // 生成されたラベル: "Last name" ));
自動的なラベルの生成はとても便利ですが、フレームワークでは、setLabels()
メソッドを使用してパーソナライズされたラベルを定義することもできます:
$this->widgetSchema->setLabels(array( 'name' => 'Your name', 'email' => 'Your email address', 'message' => 'Your message', ));
setLabel()
メソッドを使用して単独のラベルのみを修正することもできます:
$this->widgetSchema->setLabel('email', 'Your email address');
最後に、3章ではさらにフォームをカスタマイズするために、テンプレートからラベルを拡張する方法を説明します。
生成されたテーブルを越えて
フォームの表示はデフォルトでHTMLのテーブルですが、レイアウトのフォーマットは変更できます。sfWidgetFormSchemaFormatter
を継承するクラスで、異なるタイプのレイアウトフォーマットが定義されています。デフォルトでは、フォームはsfWidgetFormSchemaFormatterTable
クラスで定義されたtable
フォーマットを利用します。list
フォーマットも利用できます:
class ContactForm extends sfForm { public function configure() { $this->setWidgets(array( 'name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), 'message' => new sfWidgetFormTextarea(), )); $this->widgetSchema->setFormFormatterName('list'); } }
これらの2つのフォーマットはデフォルトで組み込まれています。独自のフォーマットクラスを作成する方法は、5章で説明します。フォームを表示できたので、投稿を管理する方法へ進みましょう。
フォームを投稿する
リスト1-3のフォームを表示するテンプレートでは、form
タグ内で、フォームを投稿するための内部URLとしてcontact/submit
を使いました。したがって、submit
アクションをcontact
モジュールに追加する必要があります。リスト1-5では、ユーザーが入力した情報をアクションで取得し、thank you
ページにリダイレクトして単にこの情報をユーザーに表示する方法を示します。
リスト1-5 - contact
モジュールのsubmit
アクションの使い方
public function executeSubmit($request) { $this->forward404Unless($request->isMethod('post')); $params = array( 'name' => $request->getParameter('name'), 'email' => $request->getParameter('email'), 'message' => $request->getParameter('message'), ); $this->redirect('contact/thankyou?'.http_build_query($params)); } public function executeThankyou() { } // apps/frontend/modules/contact/templates/thankyouSuccess.php <ul> <li>Name: <?php echo $sf_params->get('name') ?></li> <li>Email: <?php echo $sf_params->get('email') ?></li> <li>Message: <?php echo $sf_params->get('message') ?></li> </ul>
note
http_build_query
は、パラメータの配列からURLエンコードされたクエリ文字列を生成するPHPの組み込み関数です。
executeSubmit()
メソッドは3つのアクションを実行します:
セキュリティを考慮して、HTTPの
POST
メソッドを利用してページが投稿されたことをチェックします。POST
メソッドを利用して送信されていない場合は、ユーザーは404ページにリダイレクトされます。indexSuccess
テンプレートにおいて、投稿メソッドをPOST
として宣言しました(<form ... method="POST">
):$this->forward404Unless($request->isMethod('post'));
次に、ユーザーの入力値を取得して
params
テーブルに保存します:$params = array( 'name' => $request->getParameter('name'), 'email' => $request->getParameter('email'), 'message' => $request->getParameter('message'), );
最後に、ユーザーをThank youページ(
contact/thankyou
)にリダイレクトして、入力された情報を表示します:$this->redirect('contact/thankyou?'.http_build_query($params));
ユーザーを別のページにリダイレクトする代わりにsubmitSuccess.php
テンプレートを作る選択肢もありますが、POST
メソッドによるリクエストの後でユーザーを常にリダイレクトする方がベターな習慣です:
ユーザーがThank youページをリロードする場合に再投稿を妨げます。
ユーザーが戻るボタンをクリックしても、フォームを再投稿するポップアップが表示されません。
tip
executeSubmit()
がexecuteIndex()
と異なることに気づいたでしょうか。これらのメソッドを呼び出すとき、symfonyは現在のsfRequest
オブジェクトを最初の引数としてexecuteXXX()
メソッドに渡します。PHPではすべてのパラメータを定義する必要はなく、またexecuteIndex()
ではrequest
変数は必要なかったため、定義しませんでした。
図1-5に、ユーザーとインタラクトするときのメソッドのワークフローを示します。
図1-5 - メソッドのワークフロー
note
テンプレート内でユーザーの入力を再表示するとき、XSS(クロスサイトスクリプティング)攻撃のリスクにさらされます。XSSのリスクを回避するためのエスケープ処理の実装に関する詳細な情報は、"The Definitive Guide to symfony"本のビューレイヤーの内側の章を参照してください。
フォームを投稿すると、図1-6のようなページが表示されます。
図1-6 - フォームを投稿した後に表示されるページ
params
配列を作る代わりに、ユーザーの入力情報を配列に直接取得すると便利です。リスト1-6のようにすると、フィールドの値をcontact
配列に保存するように、ウィジェットが出力するHTMLのname
属性を修正できます。
リスト1-6 - ウィジェットが出力するHTMLのname
属性の修正
class ContactForm extends sfForm { public function configure() { $this->setWidgets(array( 'name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), 'message' => new sfWidgetFormTextarea(), )); $this->widgetSchema->setNameFormat('contact[%s]'); } }
setNameFormat()
を呼び出すことで、すべてのウィジェットが出力するHTMLのname
属性を修正できます。フォームを生成するとき、%s
は自動的にフィールドの名前に置き換えられます。例えばemail
フィールドでは、name
属性はcontact[email]
になります。PHPでは、contact[email]
のようなフォーマットを含むリクエストの値に対して、配列が自動的に作成されます。このようすると、フィールドの値をcontact
配列として利用できます。
リスト1-7で示すように、request
オブジェクトから直接contact
配列を取得できます。
リスト1-7 - アクションウィジェット内のname
属性の新しいフォーマット
public function executeSubmit($request) { $this->forward404Unless($request->isMethod('post')); $this->redirect('contact/thankyou?'.http_build_query($request->getParameter('contact'))); }
フォームのHTMLソースを表示すると、name
属性だけでなく、フィールド名とフォーマットに依存したid
属性もsymfonyによって生成されていることが分かります。id
属性は、name
属性から禁止されている文字をアンダースコア(_
)に置き換えることで自動的に作成されます:
名前 | name 属性 |
id 属性 |
---|---|---|
name | contact[name] | contact_name |
contact[email] | contact_email | |
message | contact[message] | contact_message |
別の解決方法
この例では、フォームを管理するために2つのアクション: 表示に対してindex
、投稿に対してsubmit
を使いました。フォームはGET
メソッドで表示されPOST
メソッドで投稿されることを利用して、リスト1-8で示されるように2つのメソッドをindex
メソッドにマージすることもできます。
リスト1-8 - フォームで使われる2つのアクションをマージする
class contactActions extends sfActions { public function executeIndex($request) { $this->form = new ContactForm(); if ($request->isMethod('post')) { $this->redirect('contact/thankyou?'.http_build_query($request->getParameter('contact'))); } } }
indexSuccess.php
テンプレート内のフォームのaction
属性も変更します:
<form action="<?php echo url_for('contact/index') ?>" method="POST">
後で説明するように、より短くより首尾一貫してわかりやすいのでこの構文を使うことが望ましいです。
ウィジェットを設定する
ウィジェットのオプション
Webサイトが複数のWebマスターによって管理されている場合、質問内容に応じてメッセージをリダイレクトできるように、分類のドロップダウンリストを追加したいでしょう(図1-7)。リスト1-9では、sfWidgetFormSelect
ウィジェットを利用して、ドロップダウンリストのsubject
を追加します。
図1-7 - subject
フィールドをフォームに追加する
リスト1-9 - subject
フィールドをフォームに追加する
class ContactForm extends sfForm { protected static $subjects = array('Subject A', 'Subject B', 'Subject C'); public function configure() { $this->setWidgets(array( 'name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), 'subject' => new sfWidgetFormSelect(array('choices' => self::$subjects)), 'message' => new sfWidgetFormTextarea(), )); $this->widgetSchema->setNameFormat('contact[%s]'); } }
sfWidgetFormSelect
ウィジェットは、すべてのウィジェットと同じように、オプションのリストを最初の引数で受け取ります。オプションは必須もしくはオプションです。sfWidgetFormSelect
ウィジェットでは、choices
オプションは必須です。これまでに見てきたウィジェットで利用可能なオプションは下記の通りです:
ウィジェット | 必須のオプション | 追加のオプション |
---|---|---|
sfWidgetFormInput |
- | type (default to text ) |
is_hidden (default to false ) |
||
sfWidgetFormSelect |
choices |
multiple (default to false ) |
sfWidgetFormTextarea |
- | - |
tip
ウィジェットのすべてのオプションを知りたい場合、 (/api/1_1/)で、オンラインで利用できる完全なAPIドキュメントを参照できます。追加のオプションやデフォルト値も合わせて、すべてのオプションが説明されています。例えばsfWidgetFormSelect
のすべてのオプションは、次のURLで調べることができます: (/api/1_1/sfWidgetFormSelect).
ウィジェットのHTML属性
それぞれのウィジェットは、HTML属性のリストを2番目のオプション引数で受け取ることもできます。これは、生成するタグのデフォルトのHTML属性を定義するために非常に役立ちます。リスト1-10では、class
属性をemail
フィールドに追加する方法を示しています。
リスト1-10 - ウィジェットに対して属性を定義する
$emailWidget = new sfWidgetFormInput(array(), array('class' => 'email')); // 生成されたHTML <input type="text" name="contact[email]" class="email" id="contact_email" />
リスト1-11で示されるように、HTML属性を使用すると、自動的に生成されたid
属性を上書きできます。
リスト1-11 - id
属性を上書きする
$emailWidget = new sfWidgetFormInput(array(), array('class' => 'email', 'id' => 'email')); // 生成されたHTML <input type="text" name="contact[email]" class="email" id="email" />
リスト1-12で示されるように、value
属性を利用してデフォルト値をフィールドに設定することも可能です。
リスト1-12 - HTML属性経由のウィジェットのデフォルト値
$emailWidget = new sfWidgetFormInput(array(), array('value' => 'Your Email Here')); // 生成されたHTML <input type="text" name="contact[email]" value="Your Email Here" id="contact_email" />
このオプションはinput
ウィジェットに対して利用できますが、checkbox
やradio
ウィジェットでは正しく機能せず、textarea
ウィジェットでは利用できません。sfForm
クラスには、任意のタイプのウィジェットに対して統一された方法で、それぞれのフィールドのデフォルト値を定義する特別なメソッドがあります。
note
第3章で説明するレイヤーの分離を保持するために、(可能であっても)HTML属性をフォームクラスで定義するのではなく、テンプレートで定義することを推奨します。
フィールドに対してデフォルトの値を定義する
それぞれのフィールドに対してデフォルト値を定義しておくと便利です。例えば、ユーザーがフィールドにフォーカスを移すと消えるヘルプメッセージを、フィールド内に表示するときです。リスト1-13では、setDefault()
とsetDefaults()
メソッドを利用してデフォルト値を定義する方法を示しています。
リスト1-13 - setDefault()
と setDefaults()
メソッド経由のウィジェットのデフォルト値
class ContactForm extends sfForm { public function configure() { // ... $this->setDefault('email', 'Your Email Here'); $this->setDefaults(array('email' => 'Your Email Here', 'name' => 'Your Name Here')); } }
setDefault()
とsetDefaults()
メソッドは、同じフォームクラスのすべてのインスタンスで共通するデフォルト値を定義する際に役立ちます。フォームを利用する既存のオブジェクトを修正したい場合、デフォルト値はインスタンスに依存するので、動的に変更できます。リスト1-14では、sfForm
コンストラクタの最初の引数でデフォルト値を動的に設定する方法を示します。
リスト1-14 - sfForm
のコンストラクタでウィジェットのデフォルト値を設定する
public function executeIndex($request) { $this->form = new ContactForm(array('email' => 'Your Email Here', 'name' => 'Your Name Here')); // ... }
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.