gotoshin

主に学んだ事の自分メモ用です。記事に書くまでも無いような事はhttps://scrapbox.io/study-diary/に書いてます。

Play Frameworkを使ったWebアプリケーション作成 掲示板作成

ドワンゴ社 新卒エンジニア向けのScala研修資料をやっていくもの ※テスト項目の手前の段階でページが正常に表示される事を確認する

資料

Introduction · Scala研修テキスト

投稿されたメッセージのクラスを作る

package controllers

import java.time.OffsetDateTime

case class Post(body: String, date: OffsetDateTime)

ケースクラスとは

いつ使うのか

  • もし、イミュータブルなオブジェクトを使って純粋に関数的なコードを書こうとするならば、通常のクラスは使わないようにした方がよいでしょう。関数型パラダイムの主な考え方は、データ構造とそれに対する操作を分離することです。ケースクラスは、必要なメソッドを持つデータ構造の表現です。データに対する関数は、別のソフトウェアエンティティ (例: traits、object) で記述されるべきです。 これに対し、通常のクラスは、データと操作を結びつけて、ミュータビリティを提供する。この方法は、オブジェクト指向パラダイムに近い。

な、なるほど〜! 今回でいうと画面からのリクエストメッセージはイミュータブルなデータとして扱うから、caseクラスを使用していると理解した。

投稿されたメッセージの保存と取得

package controllers

object PostRepository {

  var posts: Seq[Post] = Vector()

  def findAll: Seq[Post] = posts

  def add(post: Post): Unit = { posts = posts :+ post }
}

objectで定義されている理由

自分の理解だが、関数のみを定義しフィールドを保持しないため

クラス名のsufixがRepositoryの理由

var posts: Seq[Post] = Vector

Vector
  • コレクションクラスの一つで、色んな操作において速度が安定しており、まず最初に検討すると良いクラスの一つだそうな
  • imutableなので一度データ構造を構築したら変更出来ない
    • と言いつつVector(1, 2, 3, 4, 5).updated(2, 5)みたいな例が書いてあり、更新出来そうに見える?
    • 自分の頭の中の可変・不変の定義が誤っていた...、変えてそうに見えて別コレクションを返しているそう
      • 可変コレクションおよび不変コレクション | Collections | Scala Documentation
        • Scala のコレクションは、体系的に可変および不変コレクションを区別している。可変 (mutable) コレクションは上書きしたり拡張することができる。これは副作用としてコレクションの要素を変更、追加、または削除することができることを意味する。一方、不変 (immutable) コレクションは変わることが無い。追加、削除、または更新を模倣した演算は提供されるが、全ての場合において演算は新しいコレクションを返し、古いコレクションは変わることがない。

Seq

def findAll: Seq[Post] = posts

  • 通常であればここはDBから取得した値を返すが、今回は変数を返す仕様となっている

def add(post: Post): Unit = { posts = posts :+ post }

  • 変数に対して処理を行なっている理由は上の記載と同様
Unit型とは

コントローラ

※私の環境はplay 2.8を使用したため、資料の表記と異なる

package controllers

import java.time.OffsetDateTime
import javax.inject.Inject
import play.api.data.Form
import play.api.data.Forms._
import play.api.i18n.I18nSupport
import play.api.mvc.{AbstractController, ControllerComponents}

case class PostRequest(body: String)

class TextboardController @Inject()(cc: ControllerComponents) extends AbstractController(cc) with I18nSupport {
  private[this] val form = Form(
    mapping(
      "post" -> text(minLength = 1, maxLength = 10).withPrefix("hogeika")
    )(PostRequest.apply)(PostRequest.unapply))

  def get = Action { implicit request =>
    Ok(views.html.index(PostRepository.findAll, form))
  }

  def post = Action { implicit request =>
    form.bindFromRequest().fold(
      error => BadRequest(views.html.index(PostRepository.findAll, error)),
      postRequest => {
        val post = Post(postRequest.body, OffsetDateTime.now)
        PostRepository.add(post)
        Redirect("/")
      }
    )
  }
}

PlayのForm

  private[this] val form = Form(
    mapping(
      "post" -> text(minLength = 1, maxLength = 10)
    )(PostRequest.apply)(PostRequest.unapply))
(PostRequest.apply)(PostRequest.unapply))

– unapply - applyは引数を受け取りオブジェクトを返す、unapplyはオブジェクトから引数を返す - つまりapplyで値を適用し、unapplyで値を取り出す、一度applyしているため型安全が保証されると理解した

GETのときの動作
  • 投稿されたメッセージを全て読み出して、htmlテンプレートを呼び出す
POSTのときの動作
  • bindFromRequestを使ってリクエストからパラメータを読み取る
  • foldメソッドを使って結果を処理
    • 第一引数がバリデーション失敗時に呼び出す関数
    • 第二引数がバリデーション成功時に呼び出す関数
@Inject()とは

HTMLテンプレート

@import controllers.Post
@import java.time.format.DateTimeFormatter

@(posts: Seq[Post], form: Form[PostRequest])(implicit messages: Messages)
<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>Scala Text Textboard</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="/assets/textboard.css">
  </head>
  <body>
    <div class="container">
      <h1>@Messages("textboard.title")</h1>
      @for(error <- form.errors){
        <p class="text-danger" id="error">@Messages(error.message)</p>
      }
      <form method="POST" action="/" class="form-inline">
        <input type="text" class="form-control" id="post" name="post">
        <button type="submit" class="btn btn-default">@Messages("textboard.send")</button>
      </form>
      <table class="table">
        <thead>
          <tr><th>@Messages("textboard.dateTime")</th><th>@Messages("textboard.message")</th></tr>
        </thead>
        <tbody>
          @for(post <- posts.reverse){
            <tr>
              <td class="post-date">@{
                val formatter = DateTimeFormatter.ofPattern(Messages("textboard.dateFormat"), messages.lang.toLocale)
                post.date.format(formatter)
              }</td>
              <td class="post-body">@post.body</td>
            </tr>
          }
        </tbody>
      </table>
    </div>
  </body>
</html>
  • Twirlは @Messages のようにHTMLの中で @ が付いている場所をScalaのコードだと解釈します。

テンプレートの引数

  • index.scala.html というファイルは views.html.index という関数に変換されます。 - そんで例えばgetメソッド等のロジック内でOk(views.html.index(PostRepository.findAll, form))みたく呼び出される - テンプレートが関数に変換されるあたり関数型の思想なのかなと思った

投稿されたメッセージの表示

          @for(post <- posts.reverse){
            <tr>
              <td class="post-date">@{
                val formatter = DateTimeFormatter.ofPattern(Messages("textboard.dateFormat"), messages.lang.toLocale)
                post.date.format(formatter)
              }</td>
              <td class="post-body">@post.body</td>
            </tr>
          }

@{ } は複数行のScalaのコードを書く場合に便利な記法

その他調べたこと

シールド修飾子とは

コンパニオンオブジェクトの必要性が分からない

コンパニオンオブジェクトって何のためにいるの? for Rubyist - Qiita

applyって何だっけ?