前書き

Playでフォームから送信された値を受け取ってみる。

あと、validatorとかも噛ませてみる。

Play2.0.4。DBはMySQL5.1を使用。

@CretedDate 2012/10/28
@Versions Scala2.9.2 Play2.0.4

サンプルアプリの用意

とりあえずプロジェクト作成。

$ play new form_example
$ cd form_example

app/views/index.scala.htmlに下記のような適当なフォームを書いておく。とりあえずGETにしてるけどPOSTでももちろん可。

@main("フォーム") {
  <form action="/form_sample" method="get">
    名前:<input type="text" name="name"><br>
    年齢:<input type="text" name="age"><br>
    性別:<input type="radio" name="sex" value="male">男 <input type="radio" name="sex" value="female">女<br>
    <input type="submit" value="送信">
  </form>
}

フォームはhelperを使って書くこともできるけど、今回はその辺は扱わない。

引数を消してしまったので、app/controllers/Application.scalaの呼び出し部分も少し修正。

Ok(views.html.index("Your new application is ready."))
  ↓
Ok(views.html.index())

上のHTMLでactionに/form_sampleと指定したので、このURLの受け取り先を用意する。

conf/routesに以下の記述を追加。

GET     /form_sample                controllers.Application.formSample

フォームのmethodをpostにした場合は、上のGETの部分はPOSTにする。

あとはcontrollers.Application内にformSampleというメソッドを作って、そこにフォームの値を受け取る処理を書ていく。

app/controllers/Application.scalaを開いて、以下のメソッドを追加。

  def formSample = Action {
    Ok("ok")
  }

これで作業をする為の雛形は出来た。

こんな感じの画面。

画面イメージ

フォームの値を1つだけStringで受け取る

さて、ここから本題。上のフォームの3つの入力項目の値を受け取ってみる。

まずは簡易な例として、nameのみ受け取ってみる。

app/controllers/Application.scalaに以下のように記述。

  import play.api.data.Form
  import play.api.data.Forms._
  val form = Form( "name" -> text )
  def formSample = Action { implicit request =>
    val name = form.bindFromRequest.get
    Ok(name)
  }

これで送信ボタンを押すと、名前に入力した値が遷移先の画面に表示されるようになった。

コードの説明。

まず、Form( "name" -> text ) でnameという名前の子をtext(文字列)としてbindする設定をしている。text(文字列)の他にも、number(Int)、longNumber(Long)、date(java.util.Date)、list(ScalaのList)などをbindすることができる。

でも、2.0.4の段階ではDoubleやFloatをbindする為のメソッドは見当たらない。2.1には入りそうな感じらしい。

form.bindFromRequestでリクエストの内容をbindしている。implicit request(暗黙的にパラメータを渡すヤツ)を書いておかないとコンパイルエラーになる。

これらを書く時に同時にvalidator周りの記述もできるのだけど、その辺は後述。

bindFromRequestの結果はForm[T]が返る。ここからgetすればbindされた値(型はT)が、errorsでvalidatorとかのエラー情報が取れる。

今回はtext1つなのでgetした結果はStringで返る。

フォームの値を1つだけIntで受け取る

age(年齢)の部分は数値が入るので、それをIntで受け取ってみる。

  import play.api.data.Form
  import play.api.data.Forms._
  val form = Form("age" -> number)
  def formSample = Action { implicit request =>
    val age = form.bindFromRequest.get
    Ok(age.toString)
  }

Stringで受け取ったコードのname -> textだった部分をage -> numberに変えただけ。

これでbindしてgetした結果(変数age)の型はIntになる。

フォームの値をTupleで受け取る

フォームから飛んでくる値は3つ。classを作って受け取るのが礼儀正しい。けど、それをやる前に結果をTupleで受け取ってみる。たぷたぷ。

こんな感じ。

  import play.api.data.Form
  import play.api.data.Forms._
  val form = Form( tuple("name" -> text, "age" -> number, "sex" -> text) )
  def formSample = Action { implicit request =>
    val t = form.bindFromRequest.get
    Ok(t._1 + ", " + t._2 + ", " + t._3)
  }

tupleメソッドはFormsが持つメソッド。これを使ったFormからgetすると、結果がTupleで返る。この場合はTuple(String, Int, String)

APIを見た感じだとtupleは18個まで引数を取れる。ScalaのTupleは22個だったはずだけど、4個の差がどこから来たのだろう。

フォームの値をクラスで受け取る

Tupleだと数が多くなるといろいろ間違えやすい。「3番目がこの値で、5番目がこの値だから……」みたいな。

なので普通はこの方法で書く。

詳しいやり方はこの辺を参照。

とりあえず今回の3つの値を受け取る場合は、こんな感じで記述。

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

  case class User(name: String, age: Int, sex: String)

  val form = Form(mapping("name" -> text, "age" -> number, "sex" -> text)(User.apply)(User.unapply))

  def formSample = Action { implicit request =>
    val user = form.bindFromRequest.get
    Ok(user.toString)
  }

とりあえずname,age,sexをフィールドに持つUserという名前のクラスを宣言。

で、Formsのmappingというメソッドを使って、3つの値をUserクラスにマッピングしている。

ちなみにこのmappingメソッドも18個までしか値を取れない。18個以上の値が来る場合は、クラスを入れ子にするか、18個以上でも受け取れるように拡張するか、少し考えないといけない。同じnameにしてlistで受け取るという手もある。

listで値を受け取ってみる

フォームの値をlistで受け取ってみる。

要素のnameをname[0],name[1],name[2]のように配列っぽい感じで書けば良いらしい。

  <form action="/form_sample" method="get">
    名前1:<input type="text" name="name[0]"><br>
    名前2:<input type="text" name="name[1]"><br>
    名前3:<input type="text" name="name[2]"><br>
    年齢:<input type="text" name="age"><br>
    性別:<input type="radio" name="sex" value="male">男 <input type="radio" name="sex" value="female">女<br>
    <input type="submit" value="送信">
  </form>

あとは受け取り側のフィールドをList[String]にして(もちろんString以外でも可)、mapping時の指定をlist(text)にする。

  case class User(name: List[String], age: Int, sex: String)

  val form = Form(mapping("name" -> list(text), "age" -> number, "sex" -> text)(User.apply)(User.unapply))

  def formSample = Action { implicit request =>
    val user = form.bindFromRequest.get
    Ok(user.toString)
  }

ネストして2つのclassにmapping

フォームからの値を2つのclassにmappingすることもできる。

例えばSexの部分を文字列ではなくクラスにして、ネストした感じのクラスにしてみる。

  case class User(name: String, age: Int, sex: Sex)
  case class Sex(sex: String)

これに対してmappingを書く場合は、下記のようになる。

  val form = Form(
    mapping(
      "name" -> text, "age" -> number,
      "sex" -> mapping("sex" -> text)(Sex.apply)(Sex.unapply)
    )(User.apply)(User.unapply))

こんな感じでmappingはネストして書くことができる。

ageに数値以外を入れた場合

受け取っている値の1つ、ageはnumberを使ってIntにmappingしている。

この時、ageに数値以外を入れるとどうなるか。答えは、form.bindFromRequest.getでエラーになる。

エラー画面

うまくbindに失敗したFormインスタンスに対してgetすると上記のようなエラーになる。

ageでエラーになっているはずなので、error(age)の中身を見てみる。

  def formSample = Action { implicit request =>
    val error = form.bindFromRequest.error("age").get.message
    Ok(error)
  }

すると「error.number」という結果が表示される。

errorsに全エラーの情報が入ってるので、こんな感じで取り出すことも可能。

  def formSample = Action { implicit request =>
    val messages = form.bindFromRequest.errors.map(_.message).mkString("\n")
    Ok(messages)
  }

エラー時の分岐

エラーがあった場合はhasErrorsがtrueになる。ので、こんな分岐は可能。

  def formSample = Action { implicit request =>
    val f = form.bindFromRequest
    if (f.hasErrors) Ok("form error")
    else Ok(f.get.toString)
  }

また、foldメソッドに2つの関数を渡すと、エラーになった場合は1つ目の関数が、正常にbindできた場合は2つ目の関数が呼ばれる。

  def formSample = Action { implicit request =>
    val result = form.bindFromRequest.fold(
      errors => "error",
      value => "success")

    Ok(result)
  }

Validationを入れてみる

Validator周りの少し触れてみる。とりあえず必須チェック。

nonEmptyTextを使うと、入力がない時にエラーになる。trimはしてないようなので、空白文字が入ってる場合は通る。

  val form = Form(mapping(
    "name" -> nonEmptyText,
    "age" -> number,
    "sex" -> text)(User.apply)(User.unapply))

数値のRangeチェックとかもしてみる。ageは20〜99歳までとする場合、number.verifying(min(0), max(99))のように書く。

  //追加でConstraintsをimport
  import play.api.data.validation.Constraints._

  val form = Form(mapping(
    "name" -> nonEmptyText,
    "age" -> number.verifying(min(0), max(99)),
    "sex" -> text)(User.apply)(User.unapply))

文字数の制限とかもしてみる。名前は1文字以上10文字以下とする。minLengthmaxLengthを使う。

  val form = Form(mapping(
    "name" -> nonEmptyText.verifying(minLength(1), maxLength(10)),
    "age" -> number.verifying(min(0), max(99)),
    "sex" -> text)(User.apply)(User.unapply))

正規表現も使える。regexにパターンを指定する。

  val form = Form(mapping(
    "name" -> nonEmptyText.verifying(minLength(1), maxLength(10)),
    "age" -> number.verifying(min(0), max(99)),
    "sex" -> text.verifying(pattern("male|female".r)))(User.apply)(User.unapply))

Validationエラー時のメッセージを指定する

エラーメッセージの指定する方法が良く分からなかった。とりあえず、これで動くことは動いた。

  val form = Form(mapping(
    "name" -> text.verifying("名前入れてよ", { name => !name.isEmpty() && name.length < 11 }),
    "age" -> number.verifying("お酒は20歳から99歳まで", { age => age >= 20 && age < 100 }),
    "sex" -> text.verifying("性別おかしくない?", { _.matches("male|female") }))(User.apply)(User.unapply))

  def formSample = Action { implicit request =>
    val result = form.bindFromRequest.fold(
      errors => errors.errors.map(_.message).mkString("\n"),
      value => "success")

    Ok(result)
  }

関数型らしいフリーダムな書き方が出来てやりやすい。

あとはView側でエラーを受け取るようにして、そこで赤字でメッセージ表示すればそれっぽく動きそう。

Validatorに応じてメッセージを変更したい場合は、verifyingを重ねがけすることでいけた。

"name" -> text.verifying("名前入れてよ", { name => !name.isEmpty() }).verifying("田中さん禁止", { _ != "田中" })