PlayFrameworkのFormについて

なんだかよくわからないところが多かったのでメモ。

Form Mapping

PostされたデータとScalaのModelの間での変換を定義する。Catというケースクラスにname, color, age, addressが定義されていたとすると、

[scala]
import play.data._
import play.data.Forms

val catForm = Form(
mapping(
"name" -> text,
"color" -> text,
"age" -> number,
"address" -> text
)(Cat.apply)(Cat.unapply)
)
[/scala]

Formのマッピングは上記のように定義する。これで、Postで受け取ったname, color, age, addressというキーに対する値をそれぞれの型に変換してCatオブジェクトを生成する定義を与えることができた。

これをより厳密に調べる。ケースクラスでnewを付けない場合はapplyが呼ばれることになっている。APIを調べたらFormのapplyメソッドはdef apply(key: String): Fieldとなっていた。これ間違いじゃない?ソースを見ると

def apply[T](mapping: Mapping[T]): Form[T] = Form(mapping, Map.empty, Nil, None)

という定義になっていたので納得したが、APIのほうはこれでいいのかな…。間違ってるだけなんかな…。俺の見方が間違ってるのかな…。

まあそれはおいといて、とにかくMappingを食わせてやればいいという事で、Mappingをうまいこと作ってくれるヘルパメソッドがmapping。これはplay.api.data.Formsオブジェクトに定義されている。分かりにくい!でも使うときの事を考えたらわかり易い!やきもきする!

Formsのapiを参照すると、

def mapping[R, A1](a1: (String, Mapping[A1]))(apply: (A1) ⇒ R)(unapply: (R) ⇒ Option[A1]): Mapping[R]

と定義されていて、これがmapping[R, A1, A2, ... A18]まで定義されている。リストを除くフラットな構造では変数の数は18個までのようだ。定義から分かるように、マッピングはネスト出来る(つまり下記のように書ける)

[scala]
case class User(name: String, address: Address)
case class Address(street: String, city: String)

val userForm = Form(
mapping(
"name" -> text,
"address" -> mapping(
"street" -> text,
"city" -> text
)(Address.apply)(Address.unapply)
)(User.apply, User.unapply)
)
[/scala]

ので、実際に問題になることはなさそうだ。ちなみに、textやnumberというのはFormsで宣言されていて、それぞれMapping[String]、Mapping[Int]を返却するメソッドだ。

話を戻してmappingの定義を眺める。

[scala]def mapping[R, A1](a1: (String, Mapping[A1]))(apply: (A1) ⇒ R)(unapply: (R) ⇒ Option[A1]): Mapping[R]
[/scala]

Scalaは引数リストを複数とれる。mappingの場合は括弧が3つになる。一つ目は(String, Mapping[A1])のタプル。Scalaでは->(アロー演算子っぽいもの)でつなげるとそれは要素数2のタプルになる。たとえば、"aaa" -> 3 === ("aaa", 3)はtrueになる。射ですよというのが明示的に書けるのでそういう仕様になっているのだと思う。

二つ目の引数リストはA1からRを生成するための具体的な処理になる。もし、A1, A2と二つの値からのマッピングであれば、二つ目の引数リストは(A1, A2) => Rを要求する。なるほど。ようはここにRを生成するための処理を書けってことだな。三つ目は逆にRからタプルを生成するための処理を書けとある。だから、最初に挙げた例ではここにCat.applyとCat.unapplyが指定されている、と。なるほどなるほど。

だから、以下のようにも書けるわけですね。

[scala]
val catForm2 = Form(
mapping(
"name" -> text,
"color" -> text,
"age" -> number,
"address" -> text
){ (name, color, age, address) => Cat(name, color, age, address) }
{ c => Some((c.name, c.color, c.age, c.address))}
)
[/scala]

これは、apply、unapplyを使わずにわざわざ手書きに直したバージョン。C#erならこっちの方が理解しやすい。

Someでラップしているのは、Option[(A1, A2, A3, A4)]を要求しているため。OptionはScalaでnullをうまく扱う仕組み。C#で言うNullableと思っていただければよろしいかと。Option<T>という抽象クラスがあって、それを継承したSome<T>があり、SomeはTをラップする。何もないときはOption<Nothing>を継承したオブジェクトNoneを使う、と言う感じ。OptionにはisEmptyやgetOrElse(C#で言う ?? 演算子)などといったものが用意されている。なっるほどー。

また、重要な言語仕様として、第二引数リストで関数ひとつをとる場合は、丸かっこ()ではなくて中括弧{}で書けるようだ。ということは、ここで長い関数を書くのも難儀ではないね。厳密な言語仕様がどうなってるのかは知らん。

Formを使う

Formは上記で示した通り、名前→値の射影とModelの間で変換を行っている。これを使うためにはviewにformを渡してから以下のようにする。

[html]
@(houses: List[House], catform: Form[Cat])
@import helper._

....

@helper.form(action = routes.Application.insert) {
@inputText(catform("name"))
@inputText(catform("color"))
@inputText(catform("age"))
@select(catform("address"),
houses.map(h => h.address -> (h.address + "(" + h.size + ")") )
)
<input type="submit"/>
}

[/html]

こんな感じ。ヘルパーを使うのが普通のようだ。ヘルパーを使わない方法を今回は調べていない。そもそもあるのかも知らない。

これを実行すると以下のような画面が出る。

なんかちょっと美しくないね。余談だけど、addressじゃなくてhouse locationとか名前を付ければよかったな。まあそれはさておき、吐かれたHTMLは下記のようになっている。

[html]
<form action="/insert" method="POST" >
<dl class=" " id="name_field">
<dt><label for="name">name</label></dt>
<dd>
<input type="text" id="name" name="name" value="" />
</dd>
</dl>
<dl class=" " id="color_field">
<dt><label for="color">color</label></dt>
<dd>
<input type="text" id="color" name="color" value="" />
</dd>
</dl>
<dl class=" " id="age_field">
<dt><label for="age">age</label></dt>
<dd>
<input type="text" id="age" name="age" value="" />
</dd>
<dd class="info">Numeric</dd>
</dl>
<dl class=" " id="address_field">
<dt><label for="address">address</label></dt>
<dd>
<select id="address" name="address" >
<option value="Yokohama" >Yokohama(Huge)</option>
<option value="東京" >東京(でかい)</option>
</select>
</dd>
</dl>
<input type="submit"/>
</form>
[/html]

うーん、dlかあ…。ダメじゃない、ダメじゃないんだけどね…。CSSで頑張れば何とかなるんだけどね…。でも、dlかあ…。いや、分かるよ。dlでもいいと思うよ。でも、「普通tableっしょ」とか頭悪いやつが言い出しそうだなあ…。というか、俺もdlじゃないのが良いなあ…。

と、いう人のために、吐き出すコードをある程度制御できるらしい。Custom Field Constructorsというやつがそれで、inputタグ周辺のhtmlコードをどう出すかというテンプレートを定義できるそうだ。

でググると公式ドキュメントが出てきますが、いまいち読んでも意味が分からない。StackOverflowを見るとやっぱり「公式チュートリアル見ても意味わかんねーよ」という投稿があったので、私の英語力だけが原因でない。

色々調べた結果、以下のような感じで良さそうだ。まず、inputタグを表示するためのテンプレートを作る。ここではviews/inputTemplate.scala.htmlを作成し、以下のように書いてみた。

[html]
@(elements: helper.FieldElements)

<label >@elements.label(elements.lang)</label>@elements.input
[/html]

FieldElementsを読むと、labelやerrors, infosといったものがそれぞれあり、文字列で取り出せるようだ。じゃあどうやってエラーとか設定すんねん、という感じだが、ちょっと分からんので放っておこう。

inputはHtmlなvalである。こう書くとinputタグなんかが柔軟に出てきてくれるみたいだ。

そいで、実際にFormを表示させるviewの方では、importの次あたりに下記を書いておく。

[html]
@implicitField = @{ FieldConstructor(inputTemplate.f) }
[/html]

ちょっとこれが何を意味しているのかまだ理解不能だが、それもちょっとおいといて、すべてを棚上げしてとりあえず実行してみると、

[html]
<form action="/insert" method="POST" >
<label >name</label>
<input type="text" id="name" name="name" value="" />
<label >color</label>
<input type="text" id="color" name="color" value="" />
<label >age</label>
<input type="text" id="age" name="age" value="" />
<label >address</label>
<select id="address" name="address" >
<option value="Yokohama" >Yokohama(Huge)</option>
<option value="東京" >東京(でかい)</option>
</select>
<input type="submit"/>
</form>
[/html]

上記のようなコードが吐かれた。なるほどなるほど。よう分かりましたわ。

次はバリデーションとエラーにも挑戦したい。