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

第1章 - フォームの作成

フォームは、隠し入力、テキスト入力、選択ボックス、およびチェックボックスのようなフィールドで構成されます。この章では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章ではさらにフォームをカスタマイズするために、テンプレートからラベルを拡張する方法を説明します。

sidebar

ウィジェットのスキーマ

setWidgets()メソッドを利用すると、symfonyによってsfWidgetFormSchemaオブジェクトが作成されます。このオブジェクトは、一連のウィジェットを表すウィジェットです。この例のContactFormフォームでは、setWidgets()メソッドを呼び出しました。これは次のコードと同等です:

$this->setWidgetSchema(new sfWidgetFormSchema(array(
  'name'    => new sfWidgetFormInput(),
  'email'   => new sfWidgetFormInput(),
  'message' => new sfWidgetFormTextarea(),
)));
 
// 下記のコードもほぼ同等です:
 
$this->widgetSchema = new sfWidgetFormSchema(array(
  'name'    => new sfWidgetFormInput(),
  'email'   => new sfWidgetFormInput(),
  'message' => new sfWidgetFormTextarea(),
));

setLabels()メソッドは、widgetSchemaオブジェクトに含まれるウィジェットのコレクションに適用されます。

5章では、埋め込みフォームの管理が簡単になる"スキーマウィジェット"の概念を説明します。

生成されたテーブルを越えて

フォームの表示はデフォルトで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
email 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フィールドをフォームに追加する

<code>subject</code>フィールドをフォームに追加する

リスト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]');
  }
}

sidebar

sfWidgetFormSelectウィジェットのchoicesオプション

PHPは配列と連想配列を区別しないので、サブジェクトのリストに対して使った配列は次のコードと全く同じです:

$subjects = array(0 => 'Subject A', 1 => 'Subject B', 2 => 'Subject C');

生成されたウィジェットでは、optionタグのvalue属性として配列のキーを使用し、optionタグの内容としてキーに対応する値を使用します:

<select name="contact[subject]" id="contact_subject">
  <option value="0">Subject A</option>
  <option value="1">Subject B</option>
  <option value="2">Subject C</option>
</select>

value属性を変更するには、配列のキーを定義する必要があります:

$subjects = array('A' => 'Subject A', 'B' => 'Subject B', 'C' => 'Subject C');

次のようなHTMLテンプレートが生成されます:

<select name="contact[subject]" id="contact_subject">
  <option value="A">Subject A</option>
  <option value="B">Subject B</option>
  <option value="C">Subject C</option>
</select>

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ウィジェットに対して利用できますが、checkboxradioウィジェットでは正しく機能せず、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'));
 
  // ...
}

sidebar

XSS(クロスサイトスクリプティング)の対策

ウィジェットに対してHTML属性を設定するか、デフォルト値を定義すると、sfFormクラスではHTMLコードを生成する際にXSS攻撃に対してこれらの値を自動的に保護します。この保護はsettings.ymlファイルのescaping_strategy設定に依存しません。内容が別のメソッドによってすでに保護された場合、保護は再度適用されません。

この処理で、生成されたHTMLを無効にする可能性がある'"の文字も保護します。

下記のコードはこの保護方法の例です:

$emailWidget = new sfWidgetFormInput(array(), array(
  'value' => 'Hello "World!"',
  'class' => '<script>alert("foo")</script>',
));
 
// 生成されたHTML
<input
  value="Hello &quot;World!&quot;"
  class="&lt;script&gt;alert(&quot;foo&quot;)&lt;/script&gt;"
  type="text" name="contact[email]" id="contact_email"
/>