技術メモ

プログラミングとか電子工作とか

Phoenix入門 (第7章 Controllers その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。 Controllersの続きです。また記事3個になりそうです。

Rendering

コントローラはコンテンツ(ウェブサイトなどURLで表示されるやつ)のレンダリング方法が何種類かあるそうです。

テキスト

一番シンプルなのがtext/2関数。

def show(conn, %{"id" => id}) do
  text(conn, "Showing id #{id}")
end

ルーティングの箇所でget "/hoge/:id", HogeController :showなどでルーティングされている想定です。
params引数はパターンマッチしています。おそらくブラウザにはシンプルに"Showing id 15"などとidに応じたテキストデータがレンダリングされることでしょう。
あんまり使わないと思います。

JSON

これはたまにつかうかな?シンプルなJSONを返す時はjson/2関数だそうです。

def show(conn, %{"id" => id}) do
  json(conn, %{id: id})
end

先ほどと同様に指定のパスのidにアクセスするとその数字が帰ってくると思います。
(例えば、http://localhost:4000/hoge/15など)

{"id": "15"}

HTML(テンプレートファイル無し)

一応HTMLをハードコードすることもできるそうです。
たぶんテンプレートファイルを使った方がいいです。

def show(conn, %{"id" => id}) do
  html(conn, """
     <html>
       <head>
          <title>Passing an Id</title>
       </head>
       <body>
         <p>You sent in id #{id}</p>
       </body>
     </html>
    """)
end

HTMLのテンプレートファイルと違う箇所は#{id}なのか<%= id %>なのかといったところでしょうか。#{hogehoge}というのはElixirの文字列の中に変数を展開する書き方だったと思います。

render/3関数

今までの記事でも大体render/3関数を使ってたと思います。APIとかでなければこちらを使うのが良いでしょう。
render/3Phoenix.Controllerの関数ではなくPhoenix.Viewで定義されている関数です。(Phoenix.Controllerではエイリアスされているようです。)
一応なんども出てきてますが、render/3のサンプルも掲載します。

defmodule HelloPhoenixWeb.PageController do
  use HelloPhoenixWeb, :controller

  def show(conn, %{"messenger" => messenger}) do
    render(conn, "show.html", messenger: messenger)
  end
end

render/3が正常に動作するには命名規則に従ったviewファイルが必要です。(Adding Pagesなどを参照。)
つまり、HelloPhoenixControllerというコントローラでrender/3を使うにはHelloPhoenixViewが必要で、HelloPhoenixViewを使うにはlib/hello_phoenix_web/templates/helloというディレクトリとshow.html.eexというテンプレート(ただしここはrender関数による)が必要です。

上の例ではテンプレートに渡す変数としてmessengerを第三引数として渡していますが、Plug.Connの中に含めて渡すこともできるようです。

def index(conn, _params) do
  conn
  |> assign(:message, "Welcome Back!")
  |> render("index.html")
end

本来であればPlug.Conn.assign/3ですが、Phoenix.ControllerではPlug.Connがインポートされているようなのでassign/3として使えます。
assign/3はパイプラインで渡して複数の変数を登録しておけます。

def index(conn, _params) do
  conn
  |> assign(:message, "Welcome Back!")
  |> assign(:name, "Dweezil")
  |> render("index.html")
end

こうするとテンプレート内でmessagenameも両方使えます。

各アクションで利用する変数にデフォルト値をアサインしておきたい場合なんかもあると思います。
そういう場合はコントローラ内で変数をアサインする関数を作っておき、plugマクロで登録しておきます。

plug :assign_welcome_message, "Welcome Back"

def index(conn, _params) do
  conn
  |> assign(:message, "Welcome Forward")
  |> render("index.html")
end

defp assign_welcome_message(conn, msg) do
  assign(conn, :message, msg)
end

最初にplug :assign_welcome_message, "Welcome Back"と書いておけば全てのアクションに対して適応されるようです。
全てのアクションではなく、特定のアクションにのみ適応させることもできます。plug関数の記述の後にguard句の様な物を付けます。

defmodule HelloPhoenixWeb.PageController do
  use HelloPhoenixWeb, :controller

  plug :assign_welcome_message, "Hi!" when action in [:index, :show]
...

直感的で分かりやすいとは思います。

Sending responses directly

特にレンダリングするでもなく処理に成功したレスポンスを返したい場合はsend_resp/3が使える様です。

def index(conn, _params) do
  conn
  |> send_resp(201, "")
end

コンテンツのタイプをヘッダに加えておきたい場合はconnのパイプラインにput_resp_content_type/2を書いておきます。

def index(conn, _params) do
  conn
  |> put_resp_content_type("text/plain")
  |> send_resp(201, "")
end

201レスポンスなどは余り経験が無いためイマイチ使いどころが分かりませんが、やるときはこうするんだな~程度に心にとどめておきます。

Assigning Layouts

テンプレートのレイアウトのお話?多分テンプレート上で呼べるテンプレート的な奴だと思います。
lib/hello_phoenix_web/templates/layoutに入ってるやつです。

lib/hello_phoenix_web/viewディレクトリにLayoutViewモジュールがあることを鑑みると、普通のビューである事が分かります。おそらく内部実装でrenderする際にこのビュー+renderで指定してるビューが結合してレンダリングするのでしょう。

Phoenix.Controllerモジュールではput_layout/2関数が利用可能で第一引数がconn、第二引数がレンダリングするレイアウトのテンプレート名となっているそうです。雰囲気としてはrender/2関数に似ているのでしょう。
ただし、第二引数にfalseを渡すとレイアウトをレンダリングしない様にすることも可能だそうです。

def index(conn, _params) do
  conn
  |> put_layout(false)
  |> render("index.html")
end

こうするとlib/hello_phoenix_web/templates/layout/app.html.eexに書かれている箇所が消えて変わり果てた姿を目の当たりにするでしょう。

f:id:ysmn_deus:20190306175559p:plain

レイアウトを別の物に変更したい場合にもput_layout/2は使えるようです。
例えばapp.html.eexと同じディレクトリにadmin.html.eexを用意して(コピペして編集するのがいいでしょう)そっちを読み込ませたい場合は

def index(conn, _params) do
  conn
  |> put_layout("admin.html")
  |> render("index.html")
end

とすれば良いそうです。

Overriding Rendering Formats

あらかじめレンダリングする形式が分かっている場合はクエリ文字列を渡してやってレンダリングする形式をURLから指定することもできるそうです。
下記サンプルは新しいプロジェクトを生成した初期状態、つまりPageControllerが生成されていてPageViewもあってlib/hello_phoenix_web/templates/pageindex.html.eexがある時を想定してます。
デフォルトではコントローラにはindexのアクションが書かれています。

def index(conn, _params) do
  render(conn, "index.html")
end

ここで、lib/hello_phoenix_web/templates/pageindex.text.eeというファイルを追加して、内容を下記の通りにします。

OMG, this is actually some text.

ただのテキストです。
次に、ルーティング設定(lib/hello_phoenix_web/router.ex)にテキストフォーマットもリクエストを受ける旨を記載します。

defmodule HelloPhoenixWeb.Router do
  use HelloPhoenixWeb, :router

  pipeline :browser do
    plug :accepts, ["html", "text"] # "text"を追記
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
...

お次にコントローラのrender/2を書き換えます。

def index(conn, _params) do
  render(conn, :index) # "index.html"を:indexに
end

これだけで良いそうです。http://localhost:4000/?_format=textというクエリ文字列を付与した状態でアクセスすると、確かにテキストの方がレンダリングされます。(なにも付与しない場合は通常のページが表示されます。)

また、フォーマットだけでなく、クエリ文字列にパラメータを与えてテンプレートに適応することもできるそうです。indexアクションを変更します。

def index(conn, params) do
  render(conn, "index.text", message: params["message"])
end

テキストのテンプレートは下記のように変更しておきます。

OMG, this is actually some text. <%= @message %>

こうすることでhttp://localhost:4000/?_format=text&message=hogehogeとした際にindexアクションに渡されるparams"message"というキーで"hogehoge"が渡されるようです。
さらっと出てきてますが、クエリ処理は勝手にやってくれるみたいですね。

Setting the Content Type

Content Typeを指定してテンプレートをレンダリングしてレスポンスを送る方法。
Sending responses directlyの項目でもあったput_resp_content_type/2を使います。

def index(conn, _params) do
  conn
  |> put_resp_content_type("text/xml")
  |> render("index.xml", content: some_xml_content)
end

別段つまる所はないかと。
たぶんJSONのテンプレート決めてレンダリングするときとかにも使えそう。

Setting the HTTP Status

レスポンスにステータス番号を付与する方法。
put_resp_content_type/2と同様にconnのパイプラインにput_status/2関数を挟む感じだそうです。

def index(conn, _params) do
  conn
  |> put_status(202)
  |> render("index.html")
end

ステータスコードはアトムでも管理されているようで、該当ステータスはPlugのドキュメントを参照とのこと。

hexdocs.pm

例えばnot foundのページを明示的にレンダリングするなら下記の通り。

def index(conn, _params) do
  conn
  |> put_status(:not_found)
  |> render("index.html")
end

できるだけステータスコードはアトムで書くのがミスが減りそう。
ちゃんと書くには下記の通りにするそうです。

def index(conn, _params) do
  conn
  |> put_status(:not_found)
  |> put_view(HelloPhoenixWeb.ErrorView)
  |> render("404.html")
end

ErrorViewなんかはまだ聞いたことがないので別の章(おそらくViews)でやるのでしょう。
あまりステータスコードをハードコーディングしない方が賢明な気はします。

Redirection以降はまた次回!