【五 form提交及校驗】 1. form表單提交

概述

對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
  • datesqlDate:映射 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:

注意:上面模板的源碼都以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(它是一個擴展自MessagesProviderWrappedRequest),模板只需要提供一個隱式參數。

因爲你通常不需要直接訪問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!")
    }
  )
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章