概述
對form表單的處理是任何web application的重要環節。Play能讓你輕鬆處理簡單form,而對複雜form的處理也變得可能。
Play的form處理建立在數據綁定基礎之上。Play會查詢POST請求中可以格式化的值並將它們綁定到Form對象上。之後Play可以使用模式匹配來處理後續邏輯,如調用自定義校驗函數。
通常form直接在BaseController的實例中使用。但Form並非一定要和case class或者model匹配。因爲form僅僅用作處理輸入,因此可以對特定的POST請求使用特定的form對象。
導入
在class中導入下面的包就可以使用form了:
import play.api.data._
import play.api.data.Forms._
如果要使用校驗功能,還需要引入下面的包:
import play.api.data.validation.Constraints._
基本功能
現在來看看form操作的基礎:
- 定義form表單
- 定義表單的約束
- 在action中對錶單進行校驗
- 在view模板中展示form
- 在view模板中處理result或者errors
整個生命週期如下圖所示:
定義表單
首先定義一個包含所需屬性的case class。例如我們創建一個包含name和age屬性的UserData object:
case class UserData(name: String, age: Int)
然後定義一個如下的Form結構:
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(UserData.apply)(UserData.unapply)
)
在Forms object 中定義了一個 mapping 方法。此方法除了接受表單的字段名及約束之外還有兩個參數:apply及unapply函數。因爲UserData自身就是一個case class。因此可以直接利用UserData的 apply/unapply。
注意:因爲具體實現的原因,表單中單個元組或mapping中最多有22個屬性。如果表單的屬性數量大於22,你需要使用list或者嵌套值。
form可以從給定的Map中自動綁定變量:
val anyData = Map("name" -> "bob", "age" -> "21")
val userData = userForm.bind(anyData).get
大多數時候你會從Action的request中直接構造Form。因此它自身提供了一個方便的 bindFromRequest 方法,將request作爲一個隱式參數。如果你定義了一個implict request,bindFromRequest方法會自己找到它。
val userData = userForm.bindFromRequest.get
注意:這裏使用get有一個陷阱。如果form綁定不到數據,get方法會直接拋出異常。後面我們會展示一種更安全的做法。
當然我們並不要求你一定要使用case class。只要提供了合適的apply及unpply方法,你可以傳入任何對象,例如通過Form.tuple來使用元組來映射。但是使用case class有着下列好處:
- 方便。case class本身就設計用來保存一些簡單數據,並能配合Form提供很多開箱即用的功能。
- 強大。元組很容易使用,但是不支持自定義的apply或者unapply方法,而且只能以(_1, _2)的形式讀取數據。
- 專用。重用已有的 case classes 可能看起來比較方便。但是非專用的model可能還有業務邏輯,甚至是持久化細節,導致的直接後果就是緊耦合。此外,如果 form和model不是1:1映射,還需要手動忽略敏感字段以應對參數篡改攻擊。
定義表單約束
text 約束將空字符串視爲有效。這意味着name可以爲空,這並不是我們想要的。我們可以加上 nonEmptyText 約束。
val useFormContraint2 = 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
Froms object中已經提供的約束如下:
- text:映射 scala.String,可選minLength和maxLength配置
- nonEmptText:映射 scala.String,可選minLength和maxLength
- number:映射 scala.Int,可選 min,max 以及 strict
- longNumber:映射 scala.Long,可選min,max 以及 strict
- bigDecimal:可選 precision 以及 scale
- date,sqlDate:映射 java.util.Date ,java.sql.Date,可選 pattern 以及 timeZone
- email: 映射scala.String,使用了一個郵件的正則
- boolean:映射 scala.Boolean
- checked:映射 scala.Boolean
- optional:映射 scala.Option
定義臨時約束
可以利用 validation package 來自定義臨時約束:
val userFormConstraints = Form(
mapping(
"name" -> text.verifying(nonEmpty),
"age" -> number.verifying(min(0), max(100))
)(UserData.apply)(UserData.unapply)
)
也可以在 case class 中定義臨時約束:
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
})
)
也可以構造自定義校驗。在 自定義校驗 這裏查看更多細節。
在Action中校驗表單
現在我們看看如何在 action中處理表單校驗的錯誤。
處理校驗主要使用 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))
}
)
在失敗case中,我們使用 BadRequest 來渲染返回頁面,並將 errors 以參數的形式返回前端。如果我們使用了 view helpers(下面將會討論到),那麼屬性的錯誤將會在該屬性邊渲染。
在成功case中,我們向 routes.Application.home 發送一個Redirect,而不是一個渲染的view模板。這就是所謂的 Redirect after POST模式,它經常被用做防止重複提交。
注意:當使用 flashing等flash scope的方法時,必須使用“Redirect after POST”,因爲新的cookies只有在重定向之後的HTTP請求中才可用。
你也可以選擇 parse.form 來將request內容綁定到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))
}
失敗case時,默認將返回一個空的 BadRequest 響應。
你可以重寫這部分邏輯。下面的代碼和上面使用 bindFromRequest和fold的代碼效果完全一致:
val userPostWithErrors = Action(parse.form(userForm, onErrors = (formWithErrors: Form[UserData]) => {
implicit val messages = messagesApi.preferred(Seq(Lang.defaultLang))
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))
}
在view模板中展示form
你可以將form以參數的形式傳給模板引擎。如對於 user.scala.html來說,page頂端的header看起來是這種形式:
@(userForm: From[UserData])(implicit message: Messages)
因爲 user.scala.html 需要傳入一個form,因此在初始化渲染時可以傳入一個空的userForm:
def index = Action { implicit request =>
Ok(views.html.user(userForm))
}
要做的第一件事情是創建 form 標籤。 可以使用一個簡單的view helper來創建一個form 標籤,並根據傳入的反向路由來設置action和method參數:
@helper.form(action = routes.Application.userPost()) {
@helper.inputText(userForm("name"))
@helper.inputText(userForm("age"))
}
在 views.html.helper包中可以找到一些input helper。只需要你提供form 屬性,他們將展示相應的HTML輸入,設置值、約束以及綁定失敗後的錯誤提示。
注意: 你可以通過在模板中使用 @import helper._ 導入來避免單獨寫helpers的 @helper前綴。
下面是一些最常用的input helpers:
- form:渲染 form 元素。
- inputText:渲染 text input
- inputPassword:渲染password input
- inputDate:渲染date input
- inputFile:渲染file input
- inputRadiogroup:渲染radio input
- select:渲染select
- texterea:渲染textarea
- checkbox:渲染checkbox
- input:渲染通通用的input(需要明確的參數)
注意:上面模板的源碼都以Twirl模板的形式定義在views/helper包下,所以它們的版本依賴於Scala的源碼。可以到Github上查看views/helper的更多有用信息。
通過form helper,也可以向html中添加額外的參數:
@helper.inputText(userForm("name"), 'id -> "name", 'size -> 30)
可以使用上面提到過的 input helper 來構建html result:
@helper.input(userForm("name")) { (id, name, value, args) =>
<input type="text" name="@name" id="@id" @toHtmlArgs(args)>
}
注意:如果額外的參數以下劃線 _ 命名,則不會被被加入到生成的html中。下劃線開頭的參數保留做屬性構造器的參數。
對於複雜的表單元素,可以自定義view helpers及 自定義屬性構造器。
向Form Helpers傳遞MessagesProvider
上面的form helpers——包括輸入、複選框等——都將MessagesProvider作爲隱式參數。form handler需要使用MessagesProvider,因爲它們需要提供特定語言的錯誤消息提示。您可以在 “國際化與消息” 頁面中看到更多關於消息的信息。
有兩種方式傳遞MessagesProvider。
一、隱式將消息轉換爲Messages
如果 controller 繼承 play.api.i18n.I18nSupport,就會自動注入一個 MessageApi,並將隱式地將implicit 請求轉換爲 implicit 消息:
class MessagesController @Inject()(cc: ControllerComponents)
extends AbstractController(cc) with play.api.i18n.I18nSupport {
import play.api.data.Form
import play.api.data.Forms._
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(views.html.UserData.apply)(views.html.UserData.unapply)
)
def index = Action { implicit request =>
Ok(views.html.user(userForm))
}
}
這意味着將解析以下form模板:
@(userForm: Form[UserData])(implicit request: RequestHeader, messagesProvider: MessagesProvider)
@import helper._
@helper.form(action = routes.FormController.post()) {
@CSRF.formField @* <- takes a RequestHeader *@
@helper.inputText(userForm("name")) @* <- takes a MessagesProvider *@
@helper.inputText(userForm("age")) @* <- takes a MessagesProvider *@
}
二、使用 MessagesRequest
第二種方式是依賴注入一個 MessagesActionBuilder,它提供了 MessagesRequest:
// Example form injecting a messagesAction
class FormController @Inject()(messagesAction: MessagesActionBuilder, components: ControllerComponents)
extends AbstractController(components) {
import play.api.data.Form
import play.api.data.Forms._
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(views.html.UserData.apply)(views.html.UserData.unapply)
)
def index = messagesAction { implicit request: MessagesRequest[AnyContent] =>
Ok(views.html.messages(userForm))
}
def post() = TODO
}
這種方式非常有用,因爲要在表單中使用CSRF,模板必須能夠使用 Request(準確的說是RequestHeader)和Messages對象。 通過使用MessagesRequest(它是一個擴展自MessagesProvider的WrappedRequest),模板只需要提供一個隱式參數。
因爲你通常不需要直接訪問request body,你可以傳遞MessagesRequestHeader,而不是MessagesRequest[_]:
@(userForm: Form[UserData])(implicit request: MessagesRequestHeader)
@import helper._
@helper.form(action = routes.FormController.post()) {
@CSRF.formField @* <- takes a RequestHeader *@
@helper.inputText(userForm("name")) @* <- takes a MessagesProvider *@
@helper.inputText(userForm("age")) @* <- takes a MessagesProvider *@
}
也可以通過擴展MessagesAbstractController來將表單處理放到控制器中,從而使MessagesActionBuilder成爲默認Action,而不是將MessagesActionBuilder注入控制器。
// Form with Action extending MessagesAbstractController
class MessagesFormController @Inject()(components: MessagesControllerComponents)
extends MessagesAbstractController(components) {
import play.api.data.Form
import play.api.data.Forms._
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(views.html.UserData.apply)(views.html.UserData.unapply)
)
def index = Action { implicit request: MessagesRequest[AnyContent] =>
Ok(views.html.messages(userForm))
}
def post() = TODO
}
在view模板中展示錯誤
form中的errors採用 Map[String, FormError] 的形式。其中FromErrors包括:
- key:和屬性名稱保持一致
- message:一個message或者message的鍵
- args:message的參數
可以從form實例的下列方法來訪問form errors:
- errors:以 Seq[FormError]的形式返回所有錯誤
- globalErrors:以Seq[FormError]的形式返回所有錯誤,不包含key
- error("name"):以Option[FormError]的形式返回符合指定名稱的第一個error
- errors("name"):以Seq[FormError]的形式返回所有符合指定名稱的error
屬性的錯誤將使用form helpers自動渲染,故 @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>
與屬性無關的錯誤可以用error.format轉換爲字符串,它有一個隱式的 play.api.i18n.Messages 實例。
而那些沒有綁定到任何鍵的全局錯誤就沒有helper可以直接使用,只能在page中顯示定義如下:
@if(userForm.hasGlobalErrors) {
<ul>
@for(error <- userForm.globalErrors) {
<li>@error.format</li>
}
</ul>
}
元組映射
可以使用元組來代替 case class:
val userFormTuple = Form(
tuple(
"name" -> text,
"age" -> number
) // tuples come with built-in apply/unapply
)
使用元組有時比定義一個case class更加方便,尤其是參數數量不多的時候:
val anyData = Map("name" -> "bob", "age" -> "25")
val (name, age) = userFormTuple.bind(anyData).get
單值映射
當form只有一個參數的時候無法使用元組,這時可以使用Forms.single,這也避免了元組或者case class的額外開銷:
val singleForm = Form(
single(
"email" -> email
)
)
val emailValue = singleForm.bind(Map("email" -> "[email protected]")).get
向Form填值
有時需要用現有值填充表單,如修改數據前:
val filledForm = userForm.fill(UserData("Bob", 18))
在view helper中使用該元素時,會直接顯示預先定義的值:
@helper.inputText(filledForm("name")) @* will render value="Bob" *@
填充在需要使用 lists 或者 maps 的helpers 時特別有用。 如 select 及 inputRadioGroup helpers。可以使用 options 從這些helpers中取值:
單值表單映射可以在 select 下拉菜單中設置選中的選項:
val addressSelectForm: Form[AddressData] = Form(
mapping(
"street" -> text,
"city" -> text
)(AddressData.apply)(AddressData.unapply)
)
val selectedFormValues = AddressData(street = "Main St", city = "London")
val filledForm = addressSelectForm.fill(selectedFormValues)
當它在模板中使用時將options設置爲鍵值對列表:
@(
addressData: Form[AddressData],
cityOptions: List[(String, String)] = List("New York" -> "U.S. Office", "London" -> "U.K. Office", "Brussels" -> "E.U. Office")
)(implicit messages: Messages)
@helper.select(addressData("city"), options = cityOptions) @* Will render the selected city to be the filled value *@
@helper.inputText(addressData("street"))
第一個匹配上的值將填充到下拉框中。上面的例子裏,U.K. Office 將顯示在select中,對應的值是 London。
嵌套映射
可以使用 Form.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)
)
使用上面這種重複值時有兩種可選方案將form值綁定到 HTTP 請求。第一種方式是在參數之後添加一個空的中括號,如 "emails[]"。請求的形式如: http://foo.com/request?emails[][email protected]&emails[][email protected]。第二種方式是在中括號中增加數字,如 email[0],email[1],email[2]等。第二種方案還可以保證上傳的參數順序。
如果使用Play來生成HTML,你可以使用 repeat helper來生成任意數量的重複inputs:
@helper.inputText(myForm("name"))
@helper.repeat(myForm("emails"), min = 1) { emailField =>
@helper.inputText(emailField)
}
上面的min意味着即使當form數據爲空的時候,最少也顯示一條。
如果你想根據序號來重複,可以使用 repeatWithIndex helper:
@helper.repeatWithIndex(myForm("emails"), min = 1) { (emailField, index) =>
@helper.inputText(emailField, '_label -> ("email #" + index))
}
可選值
可以使用 Forms.optional 來定義可選值:
case class UserOptionalData(name: String, email: Option[String])
val userFormOptional = Form(
mapping(
"name" -> text,
"email" -> optional(email)
)(UserOptionalData.apply)(UserOptionalData.unapply)
)
上面的email映射爲Option[A],當沒有找到form值時返回None值。
默認值
可以使用Form#fill來爲表單填充初始數據:
val filledForm = userForm.fill(UserData("Bob", 18))
或者使用 Forms.default 來定義default mapping:
Form(
mapping(
"name" -> default(text, "Bob"),
"age" -> default(number, 18)
)(UserData.apply)(UserData.unapply)
)
請記住,默認值只使用在request中沒有提供form此屬性相應的數據時。
默認值在創建表單時不起作用。
忽略值
如果你想讓form的某個屬性變爲靜態值,可以使用 Froms.ignored:
val userFormStatic = Form(
mapping(
"id" -> ignored(23L),
"name" -> text,
"email" -> optional(email)
)(UserStaticData.apply)(UserStaticData.unapply)
)
自定義映射綁定
每個form映射都有一個隱式的 Formatter[T] 綁定器,來將字符串轉換爲指定數據類型。
case class UserCustomData(name:String, website: java.net.URL)
要將上面的website綁定爲java.net.URL,需要定義如下的form mapping:
val userFormCustom = Form(
mapping(
"name" -> text,
"website" -> of[URL]
)(UserCustomData.apply)(UserCustomData.unapply)
)
然後在代碼中提供隱式的 Formatter[java.net.URL]:
import play.api.data.format.Formatter
import play.api.data.format.Formats._
implicit object UrlFormatter extends Formatter[URL] {
override val format = Some(("format.url", Nil))
override def bind(key: String, data: Map[String, String]) = parsing(new URL(_), "error.url", Nil)(key, data)
override def unbind(key: String, value: URL) = Map(key -> value.toString)
}
注意上面的 Formats.parsing 函數,它用來捕獲在參數轉換過程中的任何異常,並註冊一個 FormError 到相應的field。
總結
下面是一個 model/controller 的例子。
首先是 case class:
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])
上面的 information 屬性類型是 Seq[ContactInformation] ,ContactInformation中又包含List[String]。下面我們來使用嵌套映射(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)
)
下面的代碼展示瞭如何用已有的contact來填充form:
def editContact = Action { implicit request =>
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)))
}
最後是 form 提交 handler:
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!")
}
)
}