English spoken conference

Symfony 5: The Fast Track

A new book to learn about developing modern Symfony 5 applications.

Support this project

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

第1章 - フォームの作成

1.4
Symfony version Language

フォームは、隠し入力、テキスト入力、セレクトボックスおよびチェックボックスなどのフィールドから構成されます。 この章では symfony のフォームフレームワークを利用してフォームを作成し、フォームフィールドを管理する方法を説明します。

この章を読むにあたり、symfony 1.3/1.4 が必要です。また、読み進めるにはプロジェクトおよび frontend アプリケーションを作っておく必要があります。 symfony プロジェクトの作り方に関して、入門の手引きをご参照ください。

はじめる前に

問い合わせフォームを symfony アプリケーションに追加することからはじめます。

図1-1は、ユーザーがメッセージを送りたい場合に表示される問い合わせフォームです。

図1-1 - 問い合わせフォーム

問い合わせフォーム

このフォームに3つのフィールドを作成します。ユーザーの名前、ユーザーのメールアドレスおよび送信するメッセージです。この練習課題では、図1-2のようにフォームに投稿された情報を単に表示することが目的です。

図1-2 - サンキューページ

サンキューページ

図1-3 - アプリケーションとユーザーのあいだのやりとり

ユーザーのスキーマとのやりとり

ウィジェット

sfForm クラスと sfWidget クラス

ユーザーはフォームを構成しているフィールドに情報を入力します。symfony では、フォームは sfForm クラスを継承するオブジェクトです。この例では、sfForm クラスを継承する ContactForm クラスを作ります。

note

sfForm はすべてのフォームの基底クラスで、フォームの設定とライフサイクルの管理を簡略化します。

configure() メソッドにウィジェットを追加するコードを記述することからフォームの設定をはじめましょう。

ウィジェットはフォームフィールドをあらわします。この例のフォームでは、3つのフィールド: nameemail および message をあらわす3つのウィジェットを追加する必要があります。 リスト1-1は ContactForm クラスの最初の実装を示します。

リスト1-1 - 3つのフィールドが用意されているフォームの ContactForm クラス

// lib/form/ContactForm.class.php
class ContactForm extends BaseForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'name'    => new sfWidgetFormInputText(),
      'email'   => new sfWidgetFormInputText(),
      'message' => new sfWidgetFormTextarea(),
    ));
  }
}

note

この本では、スペースの節約のために純粋な PHP コードの例では <?php 開きステートメントは省略します。新しい PHP ファイルを作るときには開きタグを追加してください。

ウィジェットは configure() メソッドのなかで定義します。 このメソッドは sfForm クラスのコンストラクタによって自動的に呼び出されます。

setWidgets() メソッドは、フォームに使われるウィジェットを定義するために使われます。 setWidgets() メソッドは、キーがフィールド名で値がウィジェットのオブジェクトである連想配列を受け入れます。 それぞれのウィジェットは sfWidget クラスを継承するオブジェクトです。この例では2種類のウィジェットを使いました。

  • sfWidgetFormInputText : このウィジェットは 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 テーブルの3つの <tr> ブロックとして表示されていることがわかります。このため、フォームを <table> タグで囲んでおく必要があります。それぞれの行には、<label> タグおよび <input><textarea> といったフォームタグが含まれます。

ラベル

それぞれのフィールドのラベルは自動生成されます。デフォルトでは、ラベルは次の2つのルールにしたがって、フィールド名から変換されます。先頭は大文字に、アンダースコアはスペースに置き換えられます。フィールドの名前が「_id」で終わる場合、サフィックスはラベルから削除されます。例:

$this->setWidgets(array(
  'first_name' => new sfWidgetFormInputText(), // 生成されるラベル: "First name"
  'last_name'  => new sfWidgetFormInputText(), // 生成されるラベル: "Last name"
  'author_id'  => new sfWidgetFormInputText(), // 生成されるラベル: "Author"
));

自動的なラベルの生成はとても便利ですが、フレームワークでは、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() メソッドを呼び出すと、sfWidgetFormSchema オブジェクトが生成されます。このオブジェクトは一連のウィジェットをあらわすウィジェットです。この例の ContactForm フォームでは、setWidgets() メソッドを呼び出しました。これは次のコードと同等です。

$this->setWidgetSchema(new sfWidgetFormSchema(array(
  'name'    => new sfWidgetFormInputText(),
  'email'   => new sfWidgetFormInputText(),
  'message' => new sfWidgetFormTextarea(),
)));
 
// 上記のコードは次のコードとほとんど同等です
 
$this->widgetSchema = new sfWidgetFormSchema(array(
  'name'    => new sfWidgetFormInputText(),
  'email'   => new sfWidgetFormInputText(),
  'message' => new sfWidgetFormTextarea(),
));

widgetSchema オブジェクトに格納されているウィジェットのコレクションに setLabels() メソッドが適用されます。

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

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

フォームの表示はデフォルトで HTML テーブルですが、レイアウトのフォーマットを変更できます。異なるタイプのレイアウトフォーマットが定義されています。これらのクラスは sfWidgetFormSchemaFormatter を継承します。デフォルトでは、sfWidgetFormSchemaFormatterTable クラスで定義されている table フォーマットがフォームに適用されます。list フォーマットに切り替えることもできます。

class ContactForm extends BaseForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'name'    => new sfWidgetFormInputText(),
      'email'   => new sfWidgetFormInputText(),
      'message' => new sfWidgetFormTextarea(),
    ));
 
    $this->widgetSchema->setFormFormatterName('list');
  }
}

これら2つのフォーマットはデフォルトで組み込まれています。独自のフォーマットクラスを作る方法は、5章で説明します。フォームを表示できたので、投稿を管理する方法へ進みましょう。

フォームを投稿する

リスト1-3のフォームを表示するテンプレートでは、form タグブロックのなかでフォームを投稿するための内部 URL として contact/submit を使いました。したがって、submit アクションを contact モジュールに追加する必要があります。 リスト1-5では、ユーザーが入力した情報をアクションで取得し、サンキューページにリダイレクトして単にこの情報をユーザーに表示する方法を示します。

リスト1-5 - contact モジュールの submit アクションの使い方

public function executeSubmit(sfWebRequest $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 は PHP の組み込み関数で、パラメータの配列から URL エンコードされたクエリ文字列を生成します。

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'),
    );
  • 最後に、ユーザーをサンキューページ (contact/thankyou) にリダイレクトして、入力された情報を表示します。

    $this->redirect('contact/thankyou?'.http_build_query($params));

ユーザーを別のページにリダイレクトする代わりに submitSuccess.php テンプレートを作る選択肢もありますが、POST メソッドによるリクエストの後でユーザーをつねにリダイレクトするほうがよい習慣です。

  • ユーザーがサンキューページをリロードする場合に再投稿を避けることができます。

  • ユーザーが戻るボタンをクリックしても、フォームを再投稿するポップアップが表示されません。

tip

executeSubmit() メソッドが executeIndex() メソッドと異なることにお気づきになられたでしょうか。これらのメソッドを呼び出すとき、symfony は現在の sfRequest オブジェクトを executeXXX() メソッドの第1引数に渡します。PHP ではすべてのパラメータを定義する必要はなく、また request 変数は executeIndex() メソッドに必要なかったため、定義しませんでした。

図1-5はユーザーとやりとりするときのメソッドのワークフローを示します。

図1-5 - メソッドのワークフロー

メソッドのワークフロー

note

テンプレート内でユーザーの入力を再表示するとき、XSS (クロスサイトスクリプティング) 攻撃のリスクにさらされます。XSS のリスクを回避するためのエスケープ処理の実装に関する詳細な情報は、「A Gentle Introduction to symfony」 のビューレイヤーの内側の章をご参照ください。

フォームを投稿すると、図1-6のようなページが表示されます。

図1-6 - フォームを投稿した後に表示されるページ

フォームを投稿した後に表示されるページ

params 配列を作る代わりに、ユーザーの入力情報を配列形式で直接取得できるると便利です。リスト1-6のように、フィールドの値を contact 配列に保存するように、ウィジェットが出力する name 属性を修正できます。

リスト1-6 - ウィジェットが出力する name 属性の修正

class ContactForm extends BaseForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'name'    => new sfWidgetFormInputText(),
      'email'   => new sfWidgetFormInputText(),
      '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(sfWebRequest $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(sfWebRequest $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 サイトに複数の管理者がいる場合、質問内容に応じてメッセージをリダイレクトできるように、分類のドロップダウンリストを追加したいことでしょう (図1-7)。 リスト1-9では、sfWidgetFormSelect ウィジェットを利用して、ドロップダウンリストの subject を追加します。

図1-7 - subject フィールドをフォームに追加する

subject フィールドをフォームに追加する

リスト1-9 - フォームに subject フィールドを追加する

class ContactForm extends BaseForm
{
  protected static $subjects = array('Subject A', 'Subject B', 'Subject C');
 
  public function configure()
  {
    $this->setWidgets(array(
      'name'    => new sfWidgetFormInputText(),
      'email'   => new sfWidgetFormInputText(),
      '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 ウィジェットは、すべてのウィジェットと同じように、オプションのリストを第1引数として受け入れます。オプションは必須もしくはオプションです。sfWidgetFormSelect ウィジェットでは、choices オプションは必須です。これまでに見てきたウィジェットで利用可能なオプションは下記のとおりです。

ウィジェット 必須のオプション 追加のオプション
sfWidgetFormInputText - type (デフォルトは text)
is_hidden (デフォルトは false)
sfWidgetFormSelect choices multiple (デフォルトは false)
sfWidgetFormTextarea - -

tip

ウィジェットのすべてのオプションを知りたければ、オンラインの API ドキュメントで参照できます。追加のオプションやデフォルト値も合わせて、すべてのオプションが説明されています。たとえば sfWidgetFormSelect のすべてのオプションは、次のURLで調べることができます: (/api/1_4/sfWidgetFormSelect)

ウィジェットの HTML 属性

それぞれのウィジェットは HTML 属性のリストをオプションの第2引数にとります。これは生成されるタグのデフォルトの HTML 属性を定義するためにとても役立ちます。リスト1-10では、 email フィールドに class 属性を追加する方法を示しています。

リスト1-10 - ウィジェットに対して属性を定義する

$emailWidget = new sfWidgetFormInputText(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 sfWidgetFormInputText(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 sfWidgetFormInputText(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 BaseForm
{
  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引数をとることを示します。

リスト1-14 - sfForm のコンストラクタでウィジェットのデフォルト値を設定する

public function executeIndex(sfWebRequest $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 sfWidgetFormInputText(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"
/>