§フォームの送信
§概要
フォームの処理と送信は、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
マッピングやモデルケースクラスを使って、タプルなど好きなものを渡すことができます。しかし、フォーム専用のケースクラスを定義することには、いくつかの利点があります。
- フォーム専用のケースクラスは便利です。 ケースクラスは、データのシンプルなコンテナになるように設計されており、
Form
機能と自然にマッチした機能をデフォルトで提供します。 - フォーム専用のケースクラスは強力です。 タプルを使用するのは便利ですが、カスタムの apply や unapply メソッドは許可されず、含まれるデータは、(
_1
,_2
など) 項数によってのみ参照できます。 - フォーム固有のケースクラスは、フォームに特化したものです。 モデルケースクラスを再利用することは便利ですが、モデルには、追加のドメインロジックや、密結合につながる明細の永続化さえ含まれることがよくあります。さらに、フォームとモデルの間に直接の1対1のマッピングがない場合、パラメータタンパリング 攻撃を防止するために、機密フィールドを明示的に無視する必要があります。
§フォームに制約を定義する
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 に定義されています。
text
:scala.String
にマッピングされ、minLength
とmaxLength
をオプションに取ります。nonEmptyText
:scala.String
にマッピングされ、minLength
とmaxLength
をオプションに取ります。number
:scala.Int
にマッピングされ、min
、max
、strict
をオプションに取ります。longNumber
:scala.Long
にマッピングされ、min
、max
、strict
をオプションに取ります。bigDecimal
:precision
とscale
をパラメータに取ります。date
,sqlDate
,jodaDate
:java.util.Date
、java.sql.Date
、org.joda.time.DateTime
にマッピングされ、pattern
とtimeZone
をオプションに取ります。jodaLocalDate
:org.joda.time.LocalDate
にマッピングされ、pattern
をオプションに取ります。email
:scala.String
にマッピングされ、電子メールの正規表現を使用します。boolean
:scala.Boolean
にマッピングされます。checked
:scala.Boolean
にマッピングされます。optional
:scala.Option
にマッピングされます。
§アドホック制約の定義
バリデーションパッケージ を使用して、ケースクラスに独自のアドホック制約を定義できます。
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 レスポンスを返すことです。 独自のロジックでこの動作を無効にすることができます。例えば、次のコードは bindFromRequest
と fold
を使った前のコードと完全に同じです。
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 タグ を作成し、引数に渡したリバースルートに従って action
と method
タグのパラメータを設定します。
@helper.form(action = routes.Application.userPost()) {
@helper.inputText(userForm("name"))
@helper.inputText(userForm("age"))
}
いくつかの入力ヘルパーは views.html.helper
にあります。それらをフォームフィールドで送信し、対応する HTML 入力を表示し、値や制約を設定し、フォームバインディングが失敗したときにエラーを表示します。
メモ: テンプレートで
@import helper._
を使うと、ヘルパーの先頭に@helper.
が付かないようにすることができます。
いくつかの入力ヘルパーがありますが、最も役立つものは次のとおりです。
form
: form 要素をレンダリングします。inputText
: text input 要素をレンダリングします。inputPassword
: password input 要素をレンダリングします。inputDate
: date input 要素をレンダリングします。inputFile
: file input 要素をレンダリングします。inputRadioGroup
: radio input 要素をレンダリングします。select
: select 要素をレンダリングします。textarea
: textarea 要素をレンダリングします。checkbox
: checkbox 要素をレンダリングします。input
: 汎用的な入力要素 (明示的な引数が必要) をレンダリングします。
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
は次のような情報を持ちます。
key
: フィールドと同じである必要があります。message
: メッセージまたはメッセージキー。args
: メッセージへの引数のリスト。
フォームエラーは、バインドされたフォームインスタンスにて、次のようにアクセスされます。
errors
: すべてのエラーをSeq[FormError]
として返します。globalErrors
: キー無しのエラーをSeq[FormError]
として返します。error("name")
: key にバインドされた最初のエラーをOption[FormError]
として返します。errors("name")
: key にバインドされたすべてのエラーをSeq[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]")).get
§値の埋め込み
典型的にはデータを編集する場合など、フォームに既存の値を設定したくなる場合があります。
val filledForm = userForm.fill(UserData("Bob", 18))
これをビューヘルパーで使用すると、指定した要素の値に埋め込み値をセットすることができます。
@helper.inputText(filledForm("name")) @* will render value="Bob" *@
値の埋め込みは、select
や inputRadioGroup
ヘルパーのような、リストや値のマップを必要とするヘルパーにとって特に便利です。これらのヘルパーをリスト、マップ、ペアで評価するには 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.street
、address.city
などの名前にする必要があります。
@helper.inputText(userFormNested("name"))
@helper.inputText(userFormNested("address.street"))
@helper.inputText(userFormNested("address.city"))
§繰り返し値
フォームマッピングでは、Forms.list
か Forms.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
と、String
の List
を含んでいます。この場合、ネストされたマッピングと繰り返しのマッピング (それぞれ Forms.seq
と Forms.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 チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。