Play framework 表格提交

環境設定

  • PlayFramework: v2.4.6
  • scala : 2.11.6

先 import 以下

import play.api.data._
import play.api.data.Forms._
import play.api.data.format.Formats._

先定義一個 case class

case class 的作用是用來 encapsulate 提交的資料為一個 object, 正式名稱為 Algebraic data type, 將 data 定義為不同結溝和不同 value,以下例子為 Haskell

  • data Bool = False | True
  • data Maybe a = Nothing | Just a

{% quote %} an algebraic data type is a kind of composite type, i.e. a type formed by combining other types {% endquote %}

回到 play 中,加入 case class User(name: String, password: String) 這定義 User 有兩個 String field, 當中有當中有 name 和 password,是否很像 c 中的 struct, 但 scala 中的 Algebraic data type 不只 case class, Option 也是其中一種

然後定義一個 Form

定義一個 form 實現如何把 form 的 parameter blind to the case class

  val userForm = Form(
    mapping(
      "name" -> of[String],
      "password" -> of[String]
    )(User.apply)(User.unapply)
  )
  • 表格中的 "name" 為 String
  • 表格中的 "password" 為 String

apply 和 unapply 的作用

scala> User("jason", "password")
res6: User = User(jason,password)

scala> :t User.unapply _
User => Option[(String, String)]

scala> User.unapply(User("jason", "password"))
res8: Option[(String, String)] = Some((jason,password))

User.apply 是用作創建新 instance of the User User.unapply 是用作 extract 資料,用作 patten matching (match & case)

// when u is a User
// u is User(jason,password)
// User(name, password) actually calls `User.unapply(u)` which extract the data inside with return value: Some(name, password)
// therefore unapply method is called extractor pattern
u match {
  case User(name, password) => println(s"u is consist of $name and $password") 
  case _ => println("not matched")
}

// when o is Option
// o: Option[(String, String)] = Some((jason,password))
o match {
  case Some((name, password)) => println(s"u is consist of $name and $password") 
  case None => println("no user is found")
}

GET Request

  def login = Action(parse.form(userForm)) { request =>
    Logger.debug("User logining")
    val user = request.body;
    Ok("Got:" + s"User name: ${user.name}, User password: ${user.password}");
  }

request.body 裡就是 User 了 Result (content type: text/plain) Got: request.body.class: class controllers.UserController$User User name: jason, User password: text

Test with Curl

curl --data http://localhost:9000/api/user/login2 \
      name=jason&password=password

POST Request

 def login2 = Action(parse.urlFormEncoded) { implicit request =>
    /* Approach 1
    val body : Map[String, Seq[String]]= request.body;
    val mapping : Map[String, String] = body.mapValues(_.mkString)

    val user = userForm.bind(mapping).get;
    */

    /* Approach 2
    val user = userForm.bindFromRequest().get;
    */
    
    /* Approach 3*/
    val user = userForm.bindFromRequest(request.body).get

    // var user = userForm..get;
    Ok(s"Got User name: ${user.name}, User password: ${user.password}");
  }

比較麻煩 需要把 mapping 轉做 form 然後再 map 做 base class

Test with curl

curl -H "Content-Type:application/x-www-form-urlencoded" \
 -d 'name=jason&password=password' \
http://localhost:9000/api/user/login2

Nested field

可以定義一個 field 作為 list or seq 但需要跟隨一定規則定義 html name field

例如在文件中

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")
        )
      ),
      "mynickname" -> mapping(
        "primaryscl" -> text,
        "secondaryscl" -> text
      )
      (ContactInformation.apply)(ContactInformation.unapply)
    )
  )(Contact.apply)(Contact.unapply)
)

phones 在 html 的 name 為 informations[0].phones

<input type="text" name="informations[0].phones">
(省略........)
<input type="text" name="informations[1].phones">

primaryscl 在 html 的 name 為 mynickname.primaryscl

<input type="text" name="mynickname.primaryscl">
<input type="text" name="mynickname.secondaryscl">

Reference