Play2.0でURLに一律でパラメータを付ける
"Play 2.0"もとい"Playframewok 2.0"もとい"Play framework 2.0"です。検索しにくいんだよksg。
使ったバージョンは2.0.3です。
あまり良いやり方じゃない気がするので、良いやり方があれば教えて下さい。
目的
Play 2.0でHTMLテンプレート上でアクションを呼び出すリンクを生成する際に、リバースルーティングを使ってとてもDRYで書けて良いと思う。*1
下はサンプルのcomputer-databaseの例。
ルーティング設定ファイル(conf/routes):
... # Edit existing computer GET /computers/:id controllers.Application.edit(id:Long) POST /computers/:id controllers.Application.update(id:Long) ...
HTMLテンプレート(app/views/list.scala.html):
... <td><a href="@routes.Application.edit(computer.id.get)">@computer.name</a></td> ...
HTMLテンプレート上で@routes.Application.edit(computer.id.get)
のように関数呼び出しの形で書くと、routesファイルの定義に従ってURLを生成してくれる。URLを変更したいときは、routeファイルだけ直せばHTMLテンプレートは直すが必要ない。とてもDRYでよい。
ここで、URLに一律で特定のパラメータ(例えばguid=ONなど)を付けたいなあと思ったんだけど、スマートなやり方が思い浮かばなかった。
ダーティーなやり方が以下になります。
一律でパラメータを付加する(ダーティーな方法)
まず、URLの生成に使用されるクラスであるplay.api.mvc.Callを偽装するオブジェクトを定義する。パッケージ名は何でも良いけど、名前は「Call」である必要がある。
app/api/mvc/Call.scala:
package api.mvc import play.core.parsers.FormUrlEncodedParser object Call { def apply(method: String, url: String) = { val params = FormUrlEncodedParser.parse(url.split('?').drop(1).mkString("?")); if (params.isDefinedAt("guid")) // 追加済み new play.api.mvc.Call(method, url) else if (params.isEmpty) new play.api.mvc.Call(method, url + "?guid=ON") else new play.api.mvc.Call(method, url + "&guid=ON") } }
次に、プロジェクトファイルで、ルーティングファイルのコンパイル(ソースコード生成)時に追加するimport先に、上記のオブジェクトを読み込む設定を追加する。
project/Build.scala
... val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings( // Add your own project settings here routesImport ++= Seq("api.mvc.Call") // この行を追加 ) ...
ここでconf/routesファイルを更新しておく。
playでプロジェクトファイルを再読み込み or 再起動
[computer-database] $ reload [info] Loading project definition from C:\Projects\repos\Play20\samples\scala\computer-database\project [info] Set current project to computer-database (in build file:/C:/Projects/repos/Play20/samples/scala/computer-database/) [computer-database] $ run --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on port 9000... (Server started, use Ctrl+D to stop and go back to the console...)
トップページを見てみると...例えば「Computer name」と「ACE」のリンクのURLが
- Before
- /computers?s=-2
/computers/381 - After
- /computers?s=-2&guid=ON
/computers/381?guid=ON
ちゃんとついている。
解説
リバースルーティングは、routeファイルを元にリバースルーティング用のクラスを自動生成して実現してるみたい。
「target/scala-2.9.1/src_managed/」の下にソースコードが生成されている。
target/scala-2.9.1/src_managed/main/controllers/routes.java:
// @SOURCE:C:/Projects/repos/Play20/samples/scala/computer-database/conf/routes // @HASH:d3cc52ad69d3a86d9c774c3c45e645163f27e021 // @DATE:Sun Sep 23 21:32:28 JST 2012 package controllers; public class routes { public static final controllers.ReverseApplication Application = new controllers.ReverseApplication(); public static final controllers.ReverseAssets Assets = new controllers.ReverseAssets(); public static class javascript { public static final controllers.javascript.ReverseApplication Application = new controllers.javascript.ReverseApplication(); public static final controllers.javascript.ReverseAssets Assets = new controllers.javascript.ReverseAssets(); } public static class ref { public static final controllers.ref.ReverseApplication Application = new controllers.ref.ReverseApplication(); public static final controllers.ref.ReverseAssets Assets = new controllers.ref.ReverseAssets(); } }
target/scala-2.9.1/src_managed/main/routes_reverseRouting.scala:
// @SOURCE:C:/Projects/repos/Play20/samples/scala/computer-database/conf/routes // @HASH:d3cc52ad69d3a86d9c774c3c45e645163f27e021 // @DATE:Sun Sep 23 21:32:28 JST 2012 import play.core._ import play.core.Router._ import play.core.j._ import play.api.mvc._ import Router.queryString ... package controllers { ... class ReverseApplication { ... // @LINE:16 def edit(id:Long) = { Call("GET", "/computers/" + implicitly[PathBindable[Long]].unbind("id", id)) }
アクションやテンプレートからroute.Application.メソッド名(パラメータ)
と呼び出すと、routes.javaで定義されたApplicationというstaticなフィールド(中身はroutes_reverseRouting.scalaで定義されたReverseApplicationというクラス)のメソッドが呼び出され、urlがセットされたCallというケースクラスが生成されることでURLの文字列を生成している。
設定のroutesImportを追加してやることで、Callの呼び出しを挿げ替えるのが今回のハックの内容になる。
import play.api.mvc._ import api.mvc.Call // ←この行が追加される ... // @LINE:16 def edit(id:Long) = { // ↓の呼び出し先が、play.api.mvc.Callからapi.mvc.Callに変わる Call("GET", "/computers/" + implicitly[PathBindable[Long]].unbind("id", id)) }
あとは、api.mvc.CallでURLを操作して元のplay.api.mvc.Callを生成して戻してやることで、無事にパラメータを追加できる。
逆にルーティングの際はパラメータは適当に無視してくれるみたいで、ちゃんとアクションが呼ばれます。