Scala PlayFramework でアプリケーション作ってみた。② 【Controller(View)編】
Scala PlayFramework でアプリケーション作ってみた。① 【環境構築編】 - yujiro's blog
の続きです。
※ 今回はPlayframework に出現する Scala言語の文法や機能などの解説も含めてしていきますが、私がScala 初心者なのでクオリティはご了承いただき、誤りがあればご指摘いただきたいです。(もちろん自分なりに調べて理解してから解説していきます。)
ルーティング
ファイルは conf/routes
です。
GET / controllers.MainController.index # 以下ログインなしでも使えるアクション GET /users/new controllers.UsersController._new POST /users/create controllers.UsersController.create GET /users/login controllers.UsersController.login POST /users/result controllers.UsersController.result # 以下ログインしないと使えないアクション GET /users/logout controllers.UsersController.logout GET /memos controllers.MemosController.index GET /memos/new controllers.MemosController._new POST /memos/create controllers.MemosController.create GET /memos/:id/edit controllers.MemosController.edit(id: Int) POST /memos/:id/update controllers.MemosController.update(id: Int) GET /memos/:id/show controllers.MemosController.show(id: Int) POST /memos/:id/destroy controllers.MemosController.destroy(id: Int)
コメントを入れていますが、アクセスするのにログインが必要になるものとそうでないものがあります。
それらはコントローラで振り分けします。
基本的にはこちらは解説はいらないかと思います。
コントローラー (View, Form)
UsersController
をメインに解説していきます。
package controllers import javax.inject._ import play.api._ import play.api.mvc._ import models.User import forms.UserForm import play.api.data.Form import play.api.data.Forms._ import play.api.data.validation.{Constraint, Constraints, Invalid, Valid} import controllers.components.actions.AuthTrait import play.api.i18n.I18nSupport import play.api.i18n.MessagesApi import play.api.i18n.Messages import play.api.i18n.Messages.Implicits._ @Singleton class UsersController @Inject() (val messagesApi: MessagesApi) extends Controller with AuthTrait with I18nSupport { private var _user:Option[User] = None val userUnique: Constraint[String] = Constraint("constraints.userIsUnique")({ plainText => val user:Option[User] = User.where('username -> plainText).apply().headOption user match { case Some(u) => Invalid("User already exists.") case None => Valid } }) val createForm = Form( mapping ( "username" -> nonEmptyText(minLength = 4).verifying(userUnique), "password" -> nonEmptyText(minLength = 8) )(UserForm.apply)(UserForm.unapply) ) val loginForm = Form( tuple( "username" -> text, "password" -> text ) verifying ("Invalid email or password", result => result match { case (username, password) => checkIdPassword(username, password) }) ) def checkIdPassword(username: String, password: String):Boolean = { val user:Option[User] = User.where('username -> username).where('password -> password).apply().headOption _user = user user match { case Some(u) => true case None => false } } def _new = Action { Ok(views.html.users._new(createForm)) } def create() = Action { implicit request => createForm.bindFromRequest().fold( errorForm => { Ok(views.html.users._new(errorForm)) }, requestForm => { User.createWithAttributes('username -> requestForm.username, 'password -> requestForm.password) Redirect("/users/login").flashing("success" -> Messages("ユーザーを作成しました。")) } ) } def login = Action { implicit request => Ok(views.html.users.login(loginForm)) } def logout = Auth { Action { Redirect("/").withNewSession.flashing("success" -> Messages("ログアウトしました。")) } } def result() = Action { implicit request => loginForm.bindFromRequest().fold( errorForm => { Ok(views.html.users.login(errorForm)) }, requestForm => { Redirect("/memos").withSession("user_id" -> _user.get.id.toString) } ) } }
まず、コントローラ定義部分の
class UsersController @Inject() (val messagesApi: MessagesApi) extends Controller with AuthTrait with I18nSupport {
ですが、@Inject()
は アノテーションで、DI(依存性)注入するよ〜っていう印。なくてもOK。IDE使う時に色々便利になるんだと思います。
依存性注入 参考
トレイトの応用編:依存性の注入によるリファクタリング · Scala研修テキスト
また、 class で play.api.i18n.I18nSupport
というトレイトを継承すると val messagesApi: MessagesApi
という抽象メンバーの定義が必須になります。
ここでは、MessagesApi
というDependency(依存性) を UsersController
に Injection(注入)するって言うことになるかと思います。
こうすることで play.api.i18n.Messages()
が利用可能になります。
ちなみに play.api.i18n.Messages()
は国際化のために利用されるAPIです。
Playframework 2.3 までは
play.api.Play.current
をimport し、@Inject() (val messagesApi: MessagesApi)
とすることなく Messages
を使えていたのですが、
この play.api.Play.current
はバージョン2.3 で廃止になりました。
method current in object Play is deprecated: This is a static reference to application, use DI instead
アクション
各メソッドを見ていきます。
_new
create
login
logout
result
のメソッドは全てアクションです。Action
関数を利用し、その中に処理を記述しています。
logout
アクションのみ
def logout = Auth { Action { Redirect("/").withNewSession.flashing("success" -> Messages("ログアウトしました。")) } }
Auth {}
となっていますが、これはフィルターです。
logout はログインしていないと見られないアクションなので、まず Auth
を利用し、ログインしていなかったらリダイレクトさせます。
この Auth
は UsersController
が継承している with AuthTrait
の中にあります。
app/controllers/components/actions/AuthTrait.scala
にあります。
package controllers.components.actions import javax.inject._ import play.api._ import play.api.mvc._ import models.User import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.i18n.I18nSupport import play.api.i18n.MessagesApi import play.api.i18n.Messages import play.api.i18n.Messages.Implicits._ /** * 参考 : http://play-gf.blogspot.jp/2015/03/filter.html */ trait AuthTrait extends Controller with I18nSupport { class AuthRequest[A](val currentUser: Option[User], request: Request[A]) extends WrappedRequest[A](request) def redirectToLogin = Action{ Redirect("/").flashing("success" -> Messages("ログインしてください。")) } case class Auth[A] (action: Action[A], redirect: Boolean = true) extends Action[A] { def apply(request: Request[A]): Future[Result] = { val currentUser: Option[User] = request.session.get("user_id") match{ case Some(v) => User.findById(v.toLong) case None => None } if (currentUser == None) { if(redirect){ redirectToLogin(request.asInstanceOf[Request[play.api.mvc.AnyContent]]) } else { action(new AuthRequest(None, request)) } }else{ action(new AuthRequest(currentUser, request)) } } lazy val parser = action.parser } }
参考
めちゃくちゃ参考になりました。
ここではSessionをみて user_id
(resultアクションでセットしています)があれば User を探しに行き、currentUser
に代入します。パターンマッチで値がなかった場合、そこでリダイレクトしてもよいのですが、User.findById(v.toLong)
でユーザー見つからない場合(Noneが帰る)を考え、その後で None
判定しています。
ここでaction(new AuthRequest(currentUser, request))
としたものですが、MainController
で解説します。
@Singleton class MainController @Inject()(val messagesApi: MessagesApi) extends Controller with AuthTrait { def index = Auth ({ Action { implicit request => val user = request.asInstanceOf[AuthRequest[AnyContent]].currentUser Ok(views.html._main.index(user)) } }, false) }
ここでも Auth{}
としています。 AuthTrait
で定義したものは request.asInstanceOf[AuthRequest[AnyContent]].currentUser
のようにして取得します。
このMainController
の index
アクションは、ユーザーがなくてもリダイレクトせず、View側で表示を変えたかったので、
Auth
クラスに別に引数を渡して分岐しました。
MainController
の
Auth ({~},false)
AuthTrait
の
(action: Action[A], redirect: Boolean = true)
if(redirect){ redirectToLogin(request.asInstanceOf[Request[play.api.mvc.AnyContent]]) } else { action(new AuthRequest(None, request)) }
の部分です。View側では
app/views/_main/index.scala.html
@user.map{ u => ようこそ @u.username さん .getOrElse { ログイン }
としています。
Form
UsersController
に戻ってFormをみていきます。
Form は play.api.data.Form
を利用します。
val createForm = Form( mapping ( "username" -> nonEmptyText(minLength = 4).verifying(userUnique), "password" -> nonEmptyText(minLength = 8) )(UserForm.apply)(UserForm.unapply) )
Form の引数に渡すタイプにmapping
と tuple
があるのですが、mapping
の場合、展開するための自作クラスののapply が必要です。
mapping と tuple の違いなどは以下にあります。
参考
自作フォームには mapping のキーになっているものに合わせてその型を指定します。
app/forms/UserForm.scala
package forms case class UserForm( username: String, password: String )
ここではバリデーションは既存で用意されている nonEmptyText
と自分で定義したuserUnique
というメソッド(verifying(userUnique)
として使用)を使用しました。
val userUnique: Constraint[String] = Constraint("constraints.userIsUnique")({ plainText => val user:Option[User] = User.where('username -> plainText).apply().headOption user match { case Some(u) => Invalid("User already exists.") case None => Valid } })
最終的にPlayFramework の play.api.data.Forms
にある、Invalid
, Valid
を返してあげればよいっぽいです。
View を見てみます。Formの部分のみ抜粋。
app/views/users/_new.html.scala
@import helper._ @import forms.UserForm @(requestForm: Form[UserForm])(implicit messages: Messages) @implicitFieldConstructor = @{ FieldConstructor(common.f) } 〜 @form(action = routes.UsersController.create()) { <div> <label for="">ユーザー名</label> @inputText(requestForm("username"), '_showConstraints -> false, '_showErrors -> false) @if(requestForm.hasErrors) { @for(v <- requestForm.errors.collect { case v if (v.key == "username") => Messages(v.message, v.args:_*) }){ <div>@v</div> } } </div> <div> <label for="">パスワード</label> @inputPassword(requestForm("password"), '_showConstraints -> false, '_showErrors -> false) @if(requestForm.hasErrors) { @for(v <- requestForm.errors.collect { case v if (v.key == "password") => Messages(v.message, v.args:_*) }){ <div>@v</div> } } </div> <div> <input type="submit" value="ログイン" class="dimention-btn"> </div> } 〜
@implicitFieldConstructor = @{ FieldConstructor(common.f) }
は自分独自のformを使用したいときに利用するものです。
デフォルトだとdd
, dt
タグが出力されてしまったのでいれました。
app/views/helper/common.scala.html
を定義します。
とりあえずシンプルに出したかったので、
@(elements: helper.FieldElements) <div>@elements.input</div>
とだけ入れておきました。
'_showConstraints -> false, '_showErrors -> false
こちらの記述はformのルール(ここで言えば「4文字以上」とかそういうやつ)である Constraints
と バリデーションエラーである Errors
の出力をどうするかです。これも余計なタグが入ってしまって嫌だったので、出力されないようにしました。
エラーは
requestForm.errors
に全部入るのでループで回して出力するようにしました。
例えばパスワードのinputだったら、
requestForm.errors.collect { case v if (v.key == "password") => Messages(v.message, v.args:_*) }
とし、key(name属性) がpassword のもののみ List にしてしまってfor で出力しています。
Controller 側では エラーがあった場合とそうでない場合の振り分けは下記のようにします。
def create() = Action { implicit request => createForm.bindFromRequest().fold( errorForm => { Ok(views.html.users._new(errorForm)) }, requestForm => { User.createWithAttributes('username -> requestForm.username, 'password -> requestForm.password) Redirect("/users/login").flashing("success" -> Messages("ユーザーを作成しました。")) } ) }
play.api.data.Form
オブジェクトの bindFromRequest().fold
というメソッドを使用します。
そうすると
エラーが有った場合、つまりbind
できなかった場合、第一引数に渡した関数が呼び出され、うまくbind
できた場合、第二引数に渡した関数が実行されます。
次回に続きます。
Scala PlayFramework でアプリケーション作ってみた ③ 【モデル(Skinny-ORM)編】 - yujiro's blog