Documentation

§フォームの送信

§概要

フォームの処理と送信は、Web アプリケーションの重要な構成要素のひとつです。 Play には、シンプルなフォーム処理を扱いやすく、そして複雑なフォーム処理を扱うことのできる機能が搭載されています。

Play のフォーム処理アプローチは、データをバインドするという概念に基づいています。POST リクエストからデータを受け取ると、Play はフォーマットされた値を探し、それらを Form オブジェクトにバインドします。そこから、Play はバインドされたフォームを使用して、データが含まれているケースクラスを評価したり、カスタムのバリデーションを呼び出すことができます。

通常、フォームは Controller インスタンスから直接使用されます。しかし、Form 定義は、ケースクラスやモデルと正確に一致させる必要はありません。それらはただ単に入力を処理するためのものであり、異なる POST には異なる Form を使用するのが合理的です。

§インポート

フォームを使用するには、以下のパッケージをクラスにインポートします。

import play.api.data._
import play.api.data.Forms._

§フォームの基本

フォーム処理の基本は次のとおりです。

最終的な結果は次のようになります。

§フォームの定義

まず、ケースクラスを定義し、そこにフォームの中で必要な要素を含めます。ここでは、ユーザーの名前と年齢を取得したいので、UserData オブジェクトを作成します。

case class UserData(name: String, age: Int)

ケースクラスができたので、次のステップとして Form 構造を定義します。Form の関数は、フォームデータを、バインドされたケースクラスのインスタンスに変換します。それを以下のように定義します。

val userForm = Form(
  mapping(
    "name" -> text,
    "age" -> number
  )(UserData.apply)(UserData.unapply)
)

この Forms オブジェクトには、mapping メソッドが定義されています。このメソッドは、フォームの名前と制約を受け取り、apply 関数と unapply 関数の2つの関数も受け取ります。UserData はケースクラスなので、apply メソッドと unapply メソッドを直接マッピングメソッドにつなぐことができます。

メモ: フォーム処理実装のための、単一タプルまたはマッピングのフィールドの最大数は 22 個です。フォームに 22 個以上のフィールドがある場合は、リストやネストされた値を使用してフォームを分割する必要があります。

フォームは、マップが与えられた時に UserData インスタンスを生成し、バインドされた値を保持します。

val anyData = Map("name" -> "bob", "age" -> "21")
val userData = userForm.bind(anyData).get

しかし、ほとんどの場合、リクエストから提供されたデータとともに、アクションの中からフォームを使用します。Form は、暗黙的なパラメータとしてリクエストを受け取る bindFromRequest を含んでいます。暗黙的なリクエストを定義すると、bindFromRequest がそれを探します。

val userData = userForm.bindFromRequest.get

メモ: ここで get を使うことには難点があります。フォームがデータにバインドできない場合、get は例外をスローします。入力を扱うより安全な方法を、次のいくつかのセクションで示します。

フォームマッピングでは、ケースクラス以外を使用することもできます。apply と unapply メソッドが適切にマッピングされている限り、Forms.tuple マッピングやモデルケースクラスを使って、タプルなど好きなものを渡すことができます。しかし、フォーム専用のケースクラスを定義することには、いくつかの利点があります。

§フォームに制約を定義する

text 制約は、空文字列が有効であるとみなします。これは name をエラーなしで空にできることを意味しますが、これは望ましいものではありません。name が適切な値を持つことを保証するには nonEmptyText 制約を使います。

val userFormConstraints2 = Form(
  mapping(
    "name" -> nonEmptyText,
    "age" -> number(min = 0, max = 100)
  )(UserData.apply)(UserData.unapply)
)

このフォームを使用すると、フォームへの入力が制約と一致しない場合にエラーが発生します。

val boundForm = userFormConstraints2.bind(Map("bob" -> "", "age" -> "25"))
boundForm.hasErrors must beTrue

デフォルトで使える制約は Forms object に定義されています。

§アドホック制約の定義

バリデーションパッケージ を使用して、ケースクラスに独自のアドホック制約を定義できます。

val userFormConstraints = Form(
  mapping(
    "name" -> text.verifying(nonEmpty),
    "age" -> number.verifying(min(0), max(100))
  )(UserData.apply)(UserData.unapply)
)

また、ケースクラス自体にアドホック制約を定義することもできます。

def validate(name: String, age: Int) = {
  name match {
    case "bob" if age >= 18 =>
      Some(UserData(name, age))
    case "admin" =>
      Some(UserData(name, age))
    case _ =>
      None
  }
}

val userFormConstraintsAdHoc = Form(
  mapping(
    "name" -> text,
    "age" -> number
  )(UserData.apply)(UserData.unapply) verifying("Failed form constraints!", fields => fields match {
    case userData => validate(userData.name, userData.age).isDefined
  })
)

独自のカスタムバリデーションを構築するオプションもあります。詳細については、カスタムバリデーション セクションを参照してください。

§アクション内のフォームの検証

制約を使用することにより、アクション内でフォームを検証し、エラーを用いてフォームを処理することができます。

これは二つの関数を持つ fold メソッドを使って行います。一つ目の関数はバインディングが失敗した場合に呼び出され、二つ目はバインディングが成功した場合に呼び出されます。

userForm.bindFromRequest.fold(
  formWithErrors => {
    // binding failure, you retrieve the form containing errors:
    BadRequest(views.html.user(formWithErrors))
  },
  userData => {
    /* binding success, you get the actual value. */
    val newUser = models.User(userData.name, userData.age)
    val id = models.User.create(newUser)
    Redirect(routes.Application.home(id))
  }
)

失敗の場合、BadRequest のページをレンダリングし、_with errors_ をパラメータとしてページに渡します。ビューヘルパー (後述) を使用すると、フィールドにバインドされているエラーが、ページのフィールドの隣にレンダリングされます。

成功の場合、ビューテンプレートをレンダリングする代わりに routes.Application.home へのルートを持つ Redirect を送ります。このパターンは Redirect after POST と呼ばれ、フォームの重複送信を防ぐ優れた方法です。

メモ: 新しいクッキーは、リダイレクトされた HTTP リクエストの後にのみ利用可能になるので、flashing または フラッシュスコープ を持つ他のメソッドを使用する場合は、“Redirect after POST” が 必須 です。

あるいは、リクエストの内容をフォームにバインドする parse.form ボディパーサー を使うことができます。

val userPost = Action(parse.form(userForm)) { implicit request =>
  val userData = request.body
  val newUser = models.User(userData.name, userData.age)
  val id = models.User.create(newUser)
  Redirect(routes.Application.home(id))
}

失敗の場合、デフォルトの動作は空の BadRequest レスポンスを返すことです。 独自のロジックでこの動作を無効にすることができます。例えば、次のコードは bindFromRequestfold を使った前のコードと完全に同じです。

val userPostWithErrors = Action(parse.form(userForm, onErrors = (formWithErrors: Form[UserData]) => BadRequest(views.html.user(formWithErrors)))) { implicit request =>
  val userData = request.body
  val newUser = models.User(userData.name, userData.age)
  val id = models.User.create(newUser)
  Redirect(routes.Application.home(id))
}

§ビューテンプレートにフォームを表示する

フォームを作成したら、それをテンプレートエンジンで使用できるようにする必要があります。これを行うには、フォームをビューテンプレートにパラメータとして含める必要があります。user.scala.html の場合、ページ上部のヘッダーは次のようになります。

@(userForm: Form[UserData])(implicit messages: Messages)

user.scala.html にはフォームが渡される必要があるので、user.scala.html をレンダリングするときに空の userForm を最初に渡すべきです。

def index = Action {
  Ok(views.html.user(userForm))
}

最初に、form タグ を作成することができます。これは単純なビューヘルパーで、form タグ を作成し、引数に渡したリバースルートに従って actionmethod タグのパラメータを設定します。

@helper.form(action = routes.Application.userPost()) {
  @helper.inputText(userForm("name"))
  @helper.inputText(userForm("age"))
}

いくつかの入力ヘルパーは views.html.helper にあります。それらをフォームフィールドで送信し、対応する HTML 入力を表示し、値や制約を設定し、フォームバインディングが失敗したときにエラーを表示します。

メモ: テンプレートで @import helper._ を使うと、ヘルパーの先頭に @helper. が付かないようにすることができます。

いくつかの入力ヘルパーがありますが、最も役立つものは次のとおりです。

form ヘルパーと同様に、生成された Html に追加される、追加のパラメータセットを指定することができます。

@helper.inputText(userForm("name"), 'id -> "name", 'size -> 30)

上記の汎用的な input ヘルパーは、あなたが望む HTML の結果をコード化することを可能にします。

@helper.input(userForm("name")) { (id, name, value, args) =>
    <input type="text" name="@name" id="@id" @toHtmlArgs(args)>
}

メモ: _ 文字で始まらない限り、追加のパラメータはすべて生成された HTML に追加されます。 _ で始まる引数は、フィールドコンストラクタ引数 用に予約されています。

複雑なフォーム要素については、独自のカスタムビューヘルパー (views パッケージの scala クラスを使用) と カスタムフィールドコンストラクタ を作成することもできます。

§ビューテンプレートでのエラーの表示

フォームのエラーは、Map[String,FormError] の形式をとり、FormError は次のような情報を持ちます。

フォームエラーは、バインドされたフォームインスタンスにて、次のようにアクセスされます。

フィールドのエラーはフォームヘルパーを使って自動的にレンダリングされるので、エラーのある @helper.inputText は次のように表示されます。

<dl class="error" id="age_field">
    <dt><label for="age">Age:</label></dt>
    <dd><input type="text" name="age" id="age" value=""></dd>
    <dd class="error">This field is required!</dd>
    <dd class="error">Another error</dd>
    <dd class="info">Required</dd>
    <dd class="info">Another constraint</dd>
</dl>

キーにバインドされていないグローバルエラーにはヘルパーがなく、ページで明示的に定義する必要があります。

@if(userForm.hasGlobalErrors) {
  <ul>
  @for(error <- userForm.globalErrors) {
    <li>@error.message</li>
  }
  </ul>
}

§タプルによるマッピング

フィールドには、ケースクラスの代わりにタプルを使用できます。

val userFormTuple = Form(
  tuple(
    "name" -> text,
    "age" -> number
  ) // tuples come with built-in apply/unapply
)

タプルを使用すると、特に項数が少ないタプルの場合、ケースクラスを定義するよりも便利になります。

val anyData = Map("name" -> "bob", "age" -> "25")
val (name, age) = userFormTuple.bind(anyData).get

§単一のマッピング

タプルは値が複数の場合にのみ使用可能です。フォームにフィールドが1つしかない場合は Forms.single を使用して、ケースクラスまたはタプルのオーバーヘッドなしに単一の値にマップします。

val singleForm = Form(
  single(
    "email" -> email
  )
)

val emailValue = singleForm.bind(Map("email" -> "[email protected].com")).get

§値の埋め込み

典型的にはデータを編集する場合など、フォームに既存の値を設定したくなる場合があります。

val filledForm = userForm.fill(UserData("Bob", 18))

これをビューヘルパーで使用すると、指定した要素の値に埋め込み値をセットすることができます。

@helper.inputText(filledForm("name")) @* will render value="Bob" *@

値の埋め込みは、selectinputRadioGroup ヘルパーのような、リストや値のマップを必要とするヘルパーにとって特に便利です。これらのヘルパーをリスト、マップ、ペアで評価するには options を使います。

§ネストされた値

フォームマッピングでは、既存のマッピング内で Forms.mapping を使用してネストされた値を定義できます。

case class AddressData(street: String, city: String)

case class UserAddressData(name: String, address: AddressData)
val userFormNested: Form[UserAddressData] = Form(
  mapping(
    "name" -> text,
    "address" -> mapping(
      "street" -> text,
      "city" -> text
    )(AddressData.apply)(AddressData.unapply)
  )(UserAddressData.apply)(UserAddressData.unapply)
)

メモ: この方法でネストされたデータを使用している場合、ブラウザが送信するフォームの値は、 address.streetaddress.city などの名前にする必要があります。

@helper.inputText(userFormNested("name"))
@helper.inputText(userFormNested("address.street"))
@helper.inputText(userFormNested("address.city"))

§繰り返し値

フォームマッピングでは、Forms.listForms.seq を使用して繰り返し値を定義できます。

case class UserListData(name: String, emails: List[String])
val userFormRepeated = Form(
  mapping(
    "name" -> text,
    "emails" -> list(email)
  )(UserListData.apply)(UserListData.unapply)
)

このような繰り返しデータを使用する場合、HTTP リクエストでフォーム値を送信する方法が2つあります。まず、“emails[]” のように空の括弧でパラメータの末尾に追加することができます。このパラメータは、 http://foo.com/request?emails[][email protected]&emails[][email protected] のように、標準的な方法で繰り返すことができます。あるいはクライアントは、emails[0]emails[1]emails[2] などのように、配列の添字でパラメータを明示的に指定することもできます。この方法では、一連の入力の順序を維持することもできます。

フォーム HTML を生成するために Play を使用している場合、フォームに含まれるのと同じ数の emails フィールドの入力を生成するには、repeat ヘルパーを使用します。

@helper.inputText(myForm("name"))
@helper.repeat(myForm("emails"), min = 1) { emailField =>
    @helper.inputText(emailField)
}

min パラメータは、対応するフォームデータが空であっても、最小数のフィールドを表示することを可能にします。

§オプションの値

フォームマッピングでは、Forms.optional を使ってオプションの値を定義することもできます。

case class UserOptionalData(name: String, email: Option[String])
val userFormOptional = Form(
  mapping(
    "name" -> text,
    "email" -> optional(email)
  )(UserOptionalData.apply)(UserOptionalData.unapply)
)

これは、出力の Option[A] にマッピングされ、フォーム値が見つからない場合は None になります。

§デフォルト値

Form#fill を使用してフォームに初期値を設定できます。

val filledForm = userForm.fill(UserData("Bob", 18))

あるいは、Forms.default を使って番号にデフォルトのマッピングを定義することもできます。

Form(
  mapping(
    "name" -> default(text, "Bob")
    "age" -> default(number, 18)
  )(User.apply)(User.unapply)
)

§無視される値

フォームにフィールドの静的な値を持たせたい場合は、Forms.ignored を使います。

val userFormStatic = Form(
  mapping(
    "id" -> ignored(23L),
    "name" -> text,
    "email" -> optional(email)
  )(UserStaticData.apply)(UserStaticData.unapply)
)

§すべてを一緒に入れる

次に、エンティティを管理するためのモデルとコントローラの例を示します。

まず、以下のようなケースクラス Contact を与えます。

case class Contact(firstname: String,
                   lastname: String,
                   company: Option[String],
                   informations: Seq[ContactInformation])

object Contact {
  def save(contact: Contact): Int = 99
}

case class ContactInformation(label: String,
                              email: Option[String],
                              phones: List[String])

Contact は、ContactInformation 要素を持つ Seq と、StringList を含んでいます。この場合、ネストされたマッピングと繰り返しのマッピング (それぞれ Forms.seqForms.list で定義されます) を組み合わせることができます。

val contactForm: Form[Contact] = Form(

  // Defines a mapping that will handle Contact values
  mapping(
    "firstname" -> nonEmptyText,
    "lastname" -> nonEmptyText,
    "company" -> optional(text),

    // Defines a repeated mapping
    "informations" -> seq(
      mapping(
        "label" -> nonEmptyText,
        "email" -> optional(email),
        "phones" -> list(
          text verifying pattern("""[0-9.+]+""".r, error="A valid phone number is required")
        )
      )(ContactInformation.apply)(ContactInformation.unapply)
    )
  )(Contact.apply)(Contact.unapply)
)

そしてこのコードは、埋め込んだデータを使用して既存の連絡先をフォームに表示する方法を示しています。

def editContact = Action {
  val existingContact = Contact(
    "Fake", "Contact", Some("Fake company"), informations = List(
      ContactInformation(
        "Personal", Some("[email protected]"), List("01.23.45.67.89", "98.76.54.32.10")
      ),
      ContactInformation(
        "Professional", Some("[email protected]"), List("01.23.45.67.89")
      ),
      ContactInformation(
        "Previous", Some("[email protected]"), List()
      )
    )
  )
  Ok(views.html.contact.form(contactForm.fill(existingContact)))
}

最後に、フォーム送信ハンドラは次のようになります。

def saveContact = Action { implicit request =>
  contactForm.bindFromRequest.fold(
    formWithErrors => {
      BadRequest(views.html.contact.form(formWithErrors))
    },
    contact => {
      val contactId = Contact.save(contact)
      Redirect(routes.Application.showContact(contactId)).flashing("success" -> "Contact saved!")
    }
  )
}

Next: CSRF 対策


このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。