文档

§处理表单提交

§概述

表单处理和提交是任何 Web 应用程序的重要组成部分。Play 提供了一些功能,使处理简单表单变得容易,并使处理复杂表单成为可能。

Play 的表单处理方法基于数据绑定概念。当数据从 POST 请求中传入时,Play 将查找格式化的值并将它们绑定到 Form 对象。从那里,Play 可以使用绑定的表单为案例类赋值数据,调用自定义验证等等。

通常,表单直接从 BaseController 实例中使用。但是,Form 定义不必完全与案例类或模型匹配:它们纯粹用于处理输入,并且为不同的 POST 使用不同的 Form 是合理的。

§导入

要使用表单,请将以下包导入您的类

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

要使用验证和约束,请将以下包导入您的类

import play.api.data.validation.Constraints._

§表单基础

我们将介绍表单处理的基础知识

最终结果将类似于以下内容

§定义表单

首先,定义一个案例类,其中包含您想要在表单中包含的元素。这里我们想要捕获用户的姓名和年龄,因此我们创建了一个 UserData 对象

case class UserData(name: String, age: Int)
object UserData {
  def unapply(u: UserData): Option[(String, Int)] = Some((u.name, u.age))
}

现在我们有了 case 类,下一步是定义一个 Form 结构。Form 的功能是将表单数据转换为 case 类的绑定实例,我们定义如下

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

The Forms 对象定义了 mapping 方法。此方法接受表单的名称和约束,以及两个函数:apply 函数和 unapply 函数。因为 UserData 是一个 case 类,所以我们可以直接将它的 applyunapply 方法插入到 mapping 方法中。

注意:由于表单处理的实现方式,单个元组或映射的最大字段数为 22。如果您的表单中包含超过 22 个字段,则应使用列表或嵌套值来分解表单。

当给定一个 Map 时,表单将创建一个包含绑定值的 UserData 实例

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

但大多数情况下,您将在 Action 中使用表单,并使用请求提供的 data。 Form 包含 bindFromRequest,它将接受一个请求作为隐式参数。如果您定义了一个隐式请求,那么 bindFromRequest 将找到它。

val userData = userForm.bindFromRequest().get

注意:这里使用 get 有一个问题。如果表单无法绑定到数据,那么 get 将抛出异常。我们将在接下来的几节中展示一种更安全的处理输入的方法。

您不限于在表单映射中使用 case 类。只要 applyunapply 方法正确映射,您就可以传入任何您喜欢的,例如使用 Forms.tuple 映射的元组或模型 case 类。但是,专门为表单定义一个 case 类有几个优点

§在表单上定义约束

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 对象 上定义。

§定义临时约束

您可以使用 validation 包 在 case 类上定义自己的临时约束。

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

您也可以在 case 类本身定义临时约束。

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))
    }
  )

在失败的情况下,我们使用 BadRequest 渲染页面,并将带有错误的表单作为参数传递给页面。如果我们使用视图助手(下面会讨论),那么绑定到字段的任何错误都将在页面中字段旁边渲染。

在成功的情况下,我们在这里发送一个带有 routes.Application.home 路由的 Redirect,而不是渲染视图模板。这种模式被称为 POST 重定向,是防止重复提交表单的绝佳方法。

注意:使用 flashing 或其他 flash 范围 方法时,必须使用“POST 重定向”,因为新的 cookie 只有在重定向的 HTTP 请求后才会可用。

或者,您可以使用 parse.form 主体解析器,它将请求的内容绑定到您的表单。

val userPost: Action[UserData] = 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[UserData] = 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))
}

§在视图模板中显示表单

有了表单后,您需要将其提供给模板引擎。您可以将表单作为参数包含在视图模板中。对于 user.scala.html,页面顶部的标题将如下所示

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

由于 user.scala.html 需要传入一个表单,因此您应该在渲染 user.scala.html 时最初传入空的 userForm

def index: Action[AnyContent] = Action { implicit request => Ok(views.html.user(userForm)) }

首先是能够创建 表单标签。它是一个简单的视图助手,它创建一个 表单标签,并根据您传入的反向路由设置 actionmethod 标签参数

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

您可以在 views.html.helper 包中找到多个输入助手。您可以向它们提供表单字段,它们将显示相应的 HTML 输入,设置值、约束并显示表单绑定失败时的错误。

注意:您可以在模板中使用 @import helper._ 来避免在助手前面添加 @helper. 前缀。

有多个输入助手,但最有用的是

注意:每个模板的源代码都定义为 views/helper 包下的 Twirl 模板,因此打包后的版本对应于生成的 Scala 源代码。作为参考,查看 Github 上的 views/helper 包可能会有用。

form 助手一样,您可以指定一组额外的参数,这些参数将被添加到生成的 Html 中。

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

上面提到的通用 input 助手将允许您编写所需的 HTML 结果。

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

注意: 除非以_字符开头,否则所有额外的参数都将添加到生成的 Html 中。以_开头的参数保留用于字段构造函数参数

对于复杂的表单元素,您还可以创建自己的自定义视图助手(使用views包中的 Scala 类)和自定义字段构造函数

§将 MessagesProvider 传递给表单助手

上面的表单助手 - inputcheckbox 等等 - 都将MessagesProvider作为隐式参数。表单处理程序需要使用MessagesProvider,因为它们需要提供映射到请求中定义的语言的错误消息。您可以在使用 Messages 进行国际化页面中了解有关Messages的更多信息。

有两种方法可以传递所需的MessagesProvider对象。

§选项一:隐式地将请求转换为 Messages

第一种方法是使控制器扩展play.api.i18n.I18nSupport,它使用注入的MessagesApi,并将隐式地将隐式请求转换为隐式Messages

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[AnyContent] = Action { implicit request => Ok(views.html.user(userForm)) }
}

这意味着以下表单模板将被解析

@(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),只需要向模板提供一个隐式参数。

因为通常不需要请求主体,所以可以传递 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 *@
}

除了将 MessagesActionBuilder 注入到控制器中,还可以通过扩展 MessagesAbstractController 来使 MessagesActionBuilder 成为默认的 Action,从而将表单处理整合到控制器中。

// 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
}

§在视图模板中显示错误

表单中的错误采用 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>

未附加到字段的错误可以使用 error.format 转换为字符串,它需要一个隐式的 play.api.i18n.Messages 实例。

未绑定到键的全局错误没有助手,必须在页面中显式定义

@if(userForm.hasGlobalErrors) {
  <ul>
  @for(error <- userForm.globalErrors) {
    <li>@error.format</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

§使用单个进行映射

元组只有在有多个值时才有可能。如果表单中只有一个字段,请使用 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" *@

填充对于需要值列表或映射的助手特别有用,例如 selectinputRadioGroup 助手。使用 options 为这些助手提供列表、映射和对

单值表单映射可以设置选择中的选中选项
下拉菜单

val addressSelectForm: Form[HomeAddressData] = Form(
  mapping(
    "street" -> text,
    "city"   -> text
  )(HomeAddressData.apply)(HomeAddressData.unapply)
)
val selectedFormValues = HomeAddressData(street = "Main St", city = "London")
val filledForm         = addressSelectForm.fill(selectedFormValues)

当它在设置选项为一对列表的模板中使用时

@(
homeAddressData: Form[HomeAddressData], 
cityOptions: List[(String, String)] = List("New York" -> "U.S. Office", "London" -> "U.K. Office", "Brussels" -> "E.U. Office")
)(implicit messages: Messages)
@helper.select(homeAddressData("city"), options = cityOptions) @* Will render the selected city to be the filled value *@
@helper.inputText(homeAddressData("street"))

下拉菜单中将根据这对中的第一个值选择填充值。
在这种情况下,英国办事处将显示在选择中,选项的值
将是伦敦。

§嵌套值

表单映射可以通过在现有映射中使用 Forms.mapping 来定义嵌套值

case class HomeAddressData(street: String, city: String)
object HomeAddressData {
  def unapply(u: HomeAddressData): Option[(String, String)] = Some((u.street, u.city))
}

case class WorkAddressData(street: String, city: String)
object WorkAddressData {
  def unapply(w: WorkAddressData): Option[(String, String)] = Some((w.street, w.city))
}

case class UserAddressData(name: String, homeAddress: HomeAddressData, workAddress: WorkAddressData)
object UserAddressData {
  def unapply(u: UserAddressData): Option[(String, HomeAddressData, WorkAddressData)] =
    Some(u.name, u.homeAddress, u.workAddress)
}
val userFormNested: Form[UserAddressData] = Form(
  mapping(
    "name" -> text,
    "homeAddress" -> mapping(
      "street" -> text,
      "city"   -> text
    )(HomeAddressData.apply)(HomeAddressData.unapply),
    "workAddress" -> mapping(
      "street" -> text,
      "city"   -> text
    )(WorkAddressData.apply)(WorkAddressData.unapply)
  )(UserAddressData.apply)(UserAddressData.unapply)
)

注意: 当您以这种方式使用嵌套数据时,浏览器发送的表单值必须命名为 homeAddress.streethomeAddress.city 等。

@helper.inputText(userFormNested("name"))
@helper.inputText(userFormNested("homeAddress.street"))
@helper.inputText(userFormNested("homeAddress.city"))
@helper.inputText(userFormNested("workAddress.street"))
@helper.inputText(userFormNested("workAddress.city"))

§重复值

表单映射可以使用 Forms.listForms.seq 来定义重复值

case class UserListData(name: String, emails: List[String])
object UserListData {
  def unapply(u: UserListData): Option[(String, List[String])] = Some((u.name, u.emails))
}
val userFormRepeated = Form(
  mapping(
    "name"   -> text,
    "emails" -> list(email)
  )(UserListData.apply)(UserListData.unapply)
)

当您使用这种重复数据时,在 HTTP 请求中发送表单值有两种选择。首先,您可以用空方括号对作为后缀添加参数,例如“emails[]”。然后可以以标准方式重复此参数,例如 http://foo.com/request?emails[][email protected]&emails[][email protected]。或者,客户端可以使用数组下标明确地为参数命名,例如 emails[0]emails[1]emails[2] 等。这种方法还可以让您保持输入序列的顺序。

如果您使用 Play 生成表单 HTML,您可以使用 repeat 帮助程序为 emails 字段生成与表单中包含的输入数量相同的输入

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

min 参数允许您即使在相应的表单数据为空时也显示最小数量的字段。

如果您想访问字段的索引,可以使用 repeatWithIndex 帮助程序代替

@helper.repeatWithIndex(myForm("emails"), min = 1) { (emailField, index) =>
    @helper.inputText(emailField, Symbol("_label") -> ("email #" + index))
}

§可选值

表单映射还可以使用 Forms.optional 来定义可选值

case class UserOptionalData(name: String, email: Option[String])
object UserOptionalData {
  def unapply(u: UserOptionalData): Option[(String, Option[String])] = Some((u.name, u.email))
}
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)
  )(UserData.apply)(UserData.unapply)
)

请记住,默认值仅在以下情况下使用:

  1. 从数据填充 Form,例如,从请求中填充
  2. 并且该字段没有相应的 data。

创建表单时不会使用默认值。

§忽略的值

如果您希望表单对某个字段使用静态值,请使用 Forms.ignored

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

§表单映射的自定义绑定器

每个表单映射都使用隐式提供的 Formatter[T] 绑定器对象,该对象执行传入 String 表单数据到目标数据类型的转换。

case class UserCustomData(name: String, website: java.net.URL)
object UserCustomData {
  def unapply(u: UserCustomData): Option[(String, java.net.URL)] = Some((u.name, u.website))
}

要绑定到自定义类型,例如上面的示例中的 java.net.URL,请定义一个这样的表单映射

val userFormCustom = Form(
  mapping(
    "name"    -> text,
    "website" -> of[URL]
  )(UserCustomData.apply)(UserCustomData.unapply)
)

要使此方法起作用,您需要使一个隐式的 Formatter[java.net.URL] 可用于执行数据绑定/解绑。

import play.api.data.format.Formats._
import play.api.data.format.Formatter
implicit object UrlFormatter extends Formatter[URL] {
  override val format: Option[(String, Seq[Any])]           = 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 函数用于捕获将 String 转换为目标类型 T 时抛出的任何异常,并在表单字段绑定上注册一个 FormError

§综合示例

以下是一个用于管理实体的模型和控制器的示例。

给定 case 类 Contact

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

object Contact {
  def save(contact: Contact): Int = 99
  def unapply(c: Contact): Option[(String, String, Option[String], Seq[ContactInformation])] =
    Some(c.firstname, c.lastname, c.company, c.informations)
}

case class ContactInformation(label: String, email: Option[String], phones: List[String])
object ContactInformation {
  def unapply(c: ContactInformation): Option[(String, Option[String], List[String])] =
    Some(c.label, c.email, c.phones)
}

请注意,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[AnyContent] = 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)))
}

最后,以下是一个表单提交处理程序的示例

def saveContact: Action[AnyContent] = 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!")
      }
    )
}

下一步: 防止 CSRF 攻击


发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛,与社区进行交流。