Playでフォームから送信された値を受け取ってみる。
あと、validatorとかも噛ませてみる。
Play2.0.4。DBはMySQL5.1を使用。
とりあえずプロジェクト作成。
$ 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")
}
これで作業をする為の雛形は出来た。
こんな感じの画面。
さて、ここから本題。上のフォームの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で返る。
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になる。
フォームから飛んでくる値は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で受け取ってみる。
要素の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することもできる。
例えば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はネストして書くことができる。
受け取っている値の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)
}
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文字以下とする。minLengthとmaxLengthを使う。
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))
エラーメッセージの指定する方法が良く分からなかった。とりあえず、これで動くことは動いた。
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("田中さん禁止", { _ != "田中" })