Play Frameworkのかゆいところをかいてみた(メッセージリソースの分割と複数チェックボックス)

Scala大好きっ子たち、今日も元気にPlayしてますか?Scala初心者のTamaです。Play Framework2.X書きやすいし速いしいいですよね!ただ、レールからちょっと外れようとすると取っ掛かりが見つけにくいのも最近の流行に乗ってるというか…。

今日は、Typetalkの新機能の開発中に必要があって実装したちょっとした工夫を2つほど紹介します。

言語リソースファイルをディレクトリ分けしたい(メッセージリソースの分割)

Playの言語リソースファイルはたとえばデフォルト+日本語の2言語ならconfディレクトリ以下に

  • messages
  • messages_ja

というファイルを用意して、

(implicit messages:Messages)
val msg = Messages("delete.message")

と書けばいいのですよね。でもたくさんのパートを複数人で同時に開発している場合、このmessagesファイルにどんどん新しい行を追加していくことになり、gitでコンフリクトしたり、別の場所でキーがかぶったりして、

Aさん
「ちょっと、”delete.message”は”削除しました”でもう使ってるんですけど!」
「いやそれなら”deleted.message”にすべきだと思うな。ここは”削除しますがよろしいですか?”のほうが相応しい(キリッ)」
Aさん
「なにその俺ルール!キモい!ていうかそっちが”delete.confirmation”とかすればいいじゃん!」
「キモぇ※◎△□%$!!」
Bさん
「まあまあ、間をとって”delete.msg”で」
全員
「「それはない」」

みたいな不毛な争いが勃発しがちですよね。(しない)

パートごとに決まった名前をキーのプレフィックスにするルールでもいいのですが、キーがやたら長くなるし、とてもDRYじゃなくて嫌な感じです。かといっていちいちパートごとにサブプロジェクトにするのも面倒だし。

複数人の開発者でうまく別々のパートを開発するためには

  • conf/featureA/
  • conf/featureB/
  • :

のように、機能ごとのディレクトリを分けて、それぞれにmessagesmessages_jaを配置できれば幸せになれそうだ、そういう仕組が多分あるだろうと思って探したのですが、マニュアルにはそういうのは無かったので、Messagesクラスのコードを調べてこんなのを作りました。

import play.api._
import play.api.i18n._
import play.api.Play.current
import play.utils.Resources
/**
 * サブディレクトリからリソースを読み込むMessages
 */
class SubMessages(dirName:String) {
  /**
   * Messages()と同じ使い方。
   */
  def apply(key:String, params:Any*)(implicit messages:Messages):String = {
    implicit val lang = messages.lang
    messageApi(key, params:_*)
  }
  /**
   * Messages()と同じ使い方。
   */
  def apply(keys:Seq[String], params:Any*)(implicit messages:Messages):String = {
    implicit val lang = messages.lang
    messageApi(keys, params:_*)
  }
  /**
   * MessagesApi実装インスタンス
   */
  private lazy val messageApi:MessagesApi = {
    val config = current.configuration
    val env = new Environment(new java.io.File("."), getClass.getClassLoader, current.mode)
    val langs = new DefaultLangs(config)
    new DefaultMessagesApi(env, config, langs) {
      /**
       * リソースファイルをロードするパスをサブディレクトリにするだけ。
       */
      override protected def loadMessages(file: String): Map[String, String] = {
        import scala.collection.JavaConverters._
        env.classLoader.getResources(dirName + "/" + file).asScala.toList
          .filterNot(url => Resources.isDirectory(env.classLoader, url)).reverse
          .map { messageFile =>
            Messages.parse(Messages.UrlMessageSource(messageFile), messageFile.toString).fold(e => throw e, identity)
          }.foldLeft(Map.empty[String, String]) { _ ++ _ }
      }
    }
  }
}

いろいろ頑張ってたくさんコード書いた気がするのですが、結局たったこれだけになりました。DefaultMessagesApiloadMessagesメソッドをちょっぴりオーバーライドした無名クラスのインスタンスを生成してるだけです。

コンストラクタ引数でconfディレクトリからの相対パスを指定して使います。下のようにシングルトンインスタンスに持たせておくのがいいでしょう。

object FeatureA {
  val SubMessages = SubMessages("featureA")
  :
  :
}

使うところではMessagesの代わりにSubMessagesを使います。implicitのMessagesと同じ言語を参照するので特に気をつけることもありません。

(implicit messages:Messages)
val msg = FeatureA.SubMessages("welcome.message")

もちろん、他のディレクトリのmessagesファイルとキーがかぶっても全く問題ないので、そのパートのスコープ内で好きなキーを使って開発できます。

checkboxを複数表示して複数選択リストを作りたい(Helperでちょっと工夫)

PlayのFormとHelperの組み合わせは、他のフレームワークと比べてちょっと機能が少ない気がします。とりわけ困ったことに、Fieldクラスのvalue変数がString型のため、そもそも複数の値をHelperに渡せないという問題が。bindFromRequestメソッドではちゃんとリクエストから複数の値を取り出せてるのに、惜しいなあ。

selectタグの複数選択とかcheckbox複数個使った複数選択が最初から用意されていないことに驚きましたが、まあ無いものは無いので仕方ないです。

Fieldクラスで複数の値を持てるようにとか言い出すと大工事になっちゃうので、ある程度妥協して大変になり過ぎないようにサクッと作ります。

Helperを自作するのはTwirテンプレートを作るだけなのでとても簡単です。FieldConstructorも自作すると結構いろんなことできて便利ですね。
views以下適当なディレクトリで、checkboxes.scala.htmlを以下のように作成します。

@import helper._
@(field:Field, options:Seq[(String, Any)], selected:Option[Seq[String]], args: (Symbol,Any)*)(implicit handler: FieldConstructor, messages: play.api.i18n.Messages)

@import play.api.i18n.Messages.Implicits.applicationMessages

@input(field, args:_*) { (id, name, value, htmlArgs) =>
  @options.map { v =>
  
} }

他の@input()を使うHelperの場合は現在の値をfield.valueで取得できるのですが、複数の値が入るようにFormを宣言している場合はfield.valueはnullになっちゃいます。仕方ないので現在の値をselected引数でもう一回受け取る仕様にせざるを得ません。

あとはいかにして楽にselected引数を作るかですが、試行錯誤の結果、呼び出し側はこんな感じに落ち着きました。

@checkboxes(form("events"), someSelectionOptions, form.value.map(_.events), '_label -> "Event to Notify")

このぐらい(form.value.map(_.events))なら、まあ許容できるんじゃないでしょうか。

以上、ちょっとした工夫でPlayのかゆいところに手が届いてよかったよねというお話でした。


ヌーラボでは始めたばかりの言語でだって無いものは作ってやる!というエンジニアを募集しています。

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる