技術メモ

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

Phoenix入門 (第8章 Views その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。 前回のViewsの続きです。

The ErrorView

PhoenixにはErrorViewというビューが生成されています。Controllersの2つめの記事で保留にしたやつです。
ファイルはlib/hello_phoenix_web/views/error_view.exが該当します。

defmodule HelloPhoenixWeb.ErrorView do
  use HelloPhoenixWeb, :view

  def template_not_found(template, _assigns) do
    Phoenix.Controller.status_message_from_template(template)
  end
end

ドキュメントとは少々異なりますが無視して進みます。
試しに404のページを叩いてみたい訳ですが、開発モードで存在しないページを参照するとデバッグメッセージが出てしまいます。
なので一旦デバッグメッセージを出ないようにします。config/dev.exsを編集します。

use Mix.Config

config :hello_phoenix, HelloPhoenixWeb.Endpoint,
  http: [port: 4000],
  debug_errors: false, # ここ
  code_reloader: true,
...

再起動してhttp://localhost:4000/such/a/wrong/pathなど、適当に存在しないURLに飛びます。
するときちんとNot Foundと表示されました。どうやらデフォルトではプレーンテキストを返す設定になっているようです。
カスタマイズするにはErrorViewにrender関数を追記してやればいいようです。試しに文言を変えます。

def render("404.html", _assigns) do
  "Page not found"
end

変わりました。
仕組み的には、おそらくルーティング外のリクエストが来たらエンドポイント上でエラーをレンダリングするようです。
Phoenix.Endpoint.RenderErrorsというモジュールでrender/5という関数が定義されており、そこでHelloPhoenixWeb.ErrorViewが利用されているようです。

このErrorViewも普通のビューのようなものなのですが、テンプレートのディレクトlib/hello_phoenix_web/templates/errorが存在しません。
エラーページをカスタマイズするにはこのディレクトリを作成し、404.html.eexを作成する必要があるそうです。作って見ましょう。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Welcome to Phoenix!</title>
    <link rel="stylesheet" href="/css/app.css">
  </head>

  <body>
    <div class="container">
      <div class="header">
        <ul class="nav nav-pills pull-right">
          <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
        </ul>
        <span class="logo"></span>
      </div>

      <div class="phx-hero">
        <p>Sorry, the page you are looking for does not exist.</p>
      </div>

      <div class="footer">
        <p><a href="http://phoenixframework.org">phoenixframework.org</a></p>
      </div>

    </div> <!-- /container -->
    <script src="/js/app.js"></script>
  </body>
</html>

もはや普通のHTMLです。
今のままではまだPage not foundレンダリングされるだけなので、def render("404.html", _assigns) doの箇所を削除します。

defmodule HelloPhoenixWeb.ErrorView do
  use HelloPhoenixWeb, :view

  def template_not_found(template, _assigns) do
    Phoenix.Controller.status_message_from_template(template)
  end
end

何も書かないで良さそうです。この辺はPhoenix.Controller.status_message_from_template(template)がよしなにしてくれるようです。
なので、ステータスコード毎にエラーページを作成したい場合は、単純にlib/hello_phoenix_web/templates/errorを作成してエラーコードのテンプレートを放り込んでおくだけで良さそうです。

Rendering JSON

ビューでレンダリングするのはHTMLだけじゃないよ!ってお話です。
コントローラでjson/2なんて方法もありましたが、ビュー側でJSONやその他のフォーマット(CSVXML?)もレンダリングできるみたいです。

なにやらJasonというオープンソースを利用しているようで、基本的にマップからJSONレンダリングをするようです。直感的でよさそう。

コントローラでjson/2することでビューをスキップしてJSONレンダリングできますが、これではビューとコントローラの役割分担が曖昧になってしまうので、基本的にはビュー側で取り扱うのがいいそうです。僕もそう思います。

早速サンプルを見ていきましょう。まずはコントローラ側を編集します。lib/hello_phoenix_web/controllers/page_controller.ex

defmodule HelloPhoenixWeb.PageController do
  use HelloPhoenixWeb, :controller

  def show(conn, _params) do
    page = %{title: "foo"}

    render(conn, "show.json", page: page)
  end

  def index(conn, _params) do
    pages = [%{title: "foo"}, %{title: "bar"}]

    render(conn, "index.json", pages: pages)
  end
end

ほぼ同じです。レンダリングするテンプレートの指定をshow.jsonのようにjsonを指定してあげるぐらいです。
続きましてはビューの方。lib/hello_phoenix_web/views/page_view.exです。

defmodule HelloPhoenixWeb.PageView do
  use HelloPhoenixWeb, :view

  def render("index.json", %{pages: pages}) do
    %{data: render_many(pages, HelloWeb.PageView, "page.json")}
  end

  def render("show.json", %{page: page}) do
    %{data: render_one(page, HelloWeb.PageView, "page.json")}
  end

  def render("page.json", %{page: page}) do
    %{title: page.title}
  end
end

コントローラ側でrender(conn, "hoge.json", マップ)を実行すると、ビュー側のrender("hoge.json", マップ)が呼び出される仕組みです。

ここでrender_many/3render_one/3という怪しげな関数が出てきています。
これはどうやら第二引数にモジュール、第三引数に該当するrender関数の第一引数(今回で言う"page.json"を引数にとるrender関数)を指定すると、第一引数の情報を利用して別のレンダリング関数を利用したレンダリング情報を自身のレスポンスに組み込める関数のようです。
文章ではなかなか伝わりにくいと思いますのでサンプルを見ていきましょう。まず

  def render("page.json", %{page: page}) do
    %{title: page.title}
  end

は純粋にJSON情報を返すだけのレンダリング関数です。例えば、page%{title: "hoge"}が渡されてこいつが呼び出されると

{
    "title": "foo"
}

というJSONレンダリングされます。これを踏まえて

  def render("index.json", %{pages: pages}) do
    %{data: render_many(pages, HelloWeb.PageView, "page.json")}
  end

はざっくり言えば

{
    "data": ## render_many(pages, HelloWeb.PageView, "page.json")の実行結果 ##
}

という形になるはずです。
render_many(pages, HelloWeb.PageView, "page.json")render("page.json", %{page: page})を第一引数のpagesの要素数だけ実行されるようで、いわば

Enum.map(pages, fn page -> render("page.json", page) end)

と同じ様な働きのようです。よって、render("index.json", %{pages: pages})の実行結果は

{
  "data": [
    {
     "title": "foo"
    },
    {
     "title": "bar"
    },
 ]
}

となります。

同じ様に

  def render("show.json", %{page: page}) do
    %{data: render_one(page, HelloWeb.PageView, "page.json")}
  end

{
    "data": ## render_one(page, HelloWeb.PageView, "page.json")の実行結果 ##
}

となり、このrender_one/3の実行結果はrender("page.json", %{page: page})のマップの部分にそのままpageを渡した

{
  "data": {
    "title": "foo"
  }
}

となります。
一見しちめんどくさい処理の様に感じますが、pageのモデルが変わった際などには修正がしやすくミスが少ない仕組みのように思えます。
構造化されているレンダリング対象から、データベースのリレーションを意識してビューを作る際などはこれらをうまく活用すると生産性が上がりそうです。

例えば一つのページに対して著者が複数いる多対一の関係性であれば、下記のように書けます。

defmodule HelloPhoenixWeb.PageView do
  use HelloPhoenixWeb, :view
  alias HelloPhoenixWeb.AuthorView

  def render("page_with_authors.json", %{page: page}) do
    %{title: page.title,
      authors: render_many(page.authors, AuthorView, "author.json")}
  end

  def render("page.json", %{page: page}) do
    %{title: page.title}
  end
end

AuthorViewの想定ですがおそらく

  def render("author.json", %{author: author}) do
    %{author: author.name}
  end

といったところでしょう。別のビューからレンダリング関数を呼び出してくるなど拡張性は高そうです。
もし、AuthorViewの引数のパターンマッチが%{writer: writer}であれば

def render("page_with_authors.json", %{page: page}) do
  %{title: page.title,
    authors: render_many(page.authors, AuthorView, "author.json", as: :writer)}
end

ともできるそうです。

わりとRendering JSONの項目は、PhoenixAPIアプリケーションとして利用することを想定しているならマストな気がします。
とはいえPhoenixフレームワーク自体かなり分かりやすく設計されているので、さらっと流して分からなければ再読という感じでいい気がします。
マクロでブラックボックス化されている箇所が多い気がしますが、そこさえ許容してしまえば可読性と生産性を両立した便利なフレームワークなのではないでしょうか。

Phoenix入門 (第8章 Views その1)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。 今回はViewsということで、実際にレンダリング処理など一番クライアント側に近い処理の話でしょうか。
とりあえず進めて行きます。

PhoenixのViewの役割はなんと言ってもレンダリングです。HTMLをレンダリングすることもあれば、JSONレンダリングすることもあるでしょう。
基本的にレンダリングはレイアウトを含むテンプレートをレンダリングすることを意味します。(Controllersの章でもJSONXMLのテンプレートも出てきました。)

Rendering Templates

Controllersの章でも書きましたが、Phoenixは結構ガチガチな命名規則で成り立っています。
PageControllerというコントローラはPageViewが必要で、PageViewではlib/hello_phoenix_web/templates/pageというディレクトリにあるテンプレートをレンダリングします。
このディレクトリはlib/hello_phoenix_web.exview/0関数の中で変更可能ですが、あまり変更することは無いと思います。

...
  def view do
    quote do
      use Phoenix.View,
        root: "lib/hello_phoenix_web/templates", # ←これ
        namespace: HelloPhoenixWeb
...

Phoenixはプロジェクトを開始するとlib/hello_phoenix_web/views下にErrorViewLayoutViewPageViewという3つのビューを自動作成してくれます。
おそらくこれらが基本的なビューを構成する最小要素になるんでしょう。まずはLayoutViewを見てみます。

defmodule HelloPhoenixWeb.LayoutView do
  use HelloPhoenixWeb, :view
end

基本的にビューを利用する時の最小構成はこれだけでいいようです。
ただしわざわざ静的ページを生成するためにPhoenixを採用する人は少ないはずです。折角なのでレイアウトのテンプレートをいじってみます。
lib/hello_phoenix_web/templates/layout/app.html.eexを編集します。

<!- <title>Hello · Phoenix Framework</title> ->
<title><%= title() %></title>

テンプレート内で関数を呼び出してタイトルを代入します。LayoutViewを編集します。

defmodule HelloPhoenixWeb.LayoutView do
  use HelloPhoenixWeb, :view

  def title do
    "Awesome New Title!"
  end
end

タイトルが変わりました。

f:id:ysmn_deus:20190309182210p:plain

<%=%>でかこうお作法はElixirのEExというプロジェクト由来だそうです。

このテンプレートはコンパイル時に*.html.eexをビューモジュールの中に取り込んで実行時にはメモリ上に展開されているようです。
title/0をどこかで実行せずに、テンプレート内にtitle()と書けるのはこの辺の恩恵のようです。

また、Viewでuse HelloWeb, :viewをしているときはHelloPhoenixWeb.Router.HelpersRoutesとしてエイリアスされているようです。
なので、ビューやテンプレート上ではPath helperがRoutes.*_pathとして使えます。
なので、リンクを張るときは

<a href="<%= Routes.page_path(@conn, :index) %>">Link back to this page</a></p>

のように記載できます。
ただし、個人的にはビューは切り分けて別のフロントフレームワークで管理するべきでは?とも思うので、この辺は軽く流します。

More About Views

ビューの挙動についてもう少しだけ深掘りします。
lib/hello_phoenix_web/templates/page/test.html.eexというファイルを下記のように作ります。

This is the message: <%= message() %>

あとlib/hello_phoenix_web/views/page_view.exも変更します。

defmodule HelloPhoenixWeb.PageView do
  use HelloPhoenixWeb, :view

  def message do
    "Hello from the view!"
  end
end

本当はルーティングの設定も書いてやらないとブラウザでは表示されませんが、iex上で挙動を確認してみます。
iex -S mixでアプリケーションを起動して、render関数を直接叩いてやります。

iex(1)> Phoenix.View.render(HelloPhoenixWeb.PageView, "test.html", %{})
{:safe, ["This is the message: ", "Hello from the view!"]}

なにやらタプルが返ってきてます。
ここで:safeはHTMLエスケープされた情報であることを意味しているようです。(&ltとかになっちゃう奴)
なにやらPhoenix.HTML.Safeプロトコルというものの仕様だそうですが、話が長くなりそうなので割愛します。

render/3関数で第三引数に空のマップを渡してますが、これはテンプレートに渡す変数です。
試しにlib/hello_phoenix_web/templates/page/test.html.eexを編集して確認してみます。

I came from assigns: <%= @message %>
This is the message: <%= message() %>

リロードして変数を渡してみます。

iex(2)> r HelloPhoenixWeb.PageView
warning: redefining module HelloPhoenixWeb.PageView (current version loaded from _build/dev/lib/hello_phoenix/ebin/Elixir.HelloPhoenixWeb.PageView.beam)
  lib/hello_phoenix_web/views/page_view.ex:1

{:reloaded, HelloPhoenixWeb.PageView, [HelloPhoenixWeb.PageView]}
iex(3)> Phoenix.View.render(HelloPhoenixWeb.PageView, "test.html", message: "Assigns has an @.")
{:safe,
 ["I came from assigns: ", "Assigns has an @.", "\r\nThis is the message: ",
  "Hello from the view!"]}

なにやらPhoenixのドキュメントのサンプルと表示は異なってますが、意味は同じ筈です。(ドキュメントはリストがヘッダと後続リスト部で別れた表記になってる)
どうやらHTMLデータはリストで提供され、関数や変数のある箇所で区切られて処理され、最終的にリストとして渡されるという仕組みになっているようです。
あと、折角エスケープの話が出てきたのでエスケープされるような文字列も試します。

iex(4)> Phoenix.View.render(HelloPhoenixWeb.PageView, "test.html", message: "<script>badThings();</script>")
{:safe,
 [
   "I came from assigns: ",
   [
     [[[[] | "&lt;"], "script" | "&gt;"], "badThings();" | "&lt;"],
     "/script" |
     "&gt;"
   ],
   "\r\nThis is the message: ",
   "Hello from the view!"
 ]}

めちゃくちゃになっとるやん・・・
やはり何か処理が挟まる度にリストで区切られるようです。

レンダリングされた文字列(リスト)のみ必要な場合(:safeとかいらない場合)はrender_to_iodata/3という関数があるようです。

iex(5)> Phoenix.View.render_to_iodata(HelloPhoenixWeb.PageView, "test.html", message: "Assigns has an @.")
["I came from assigns: ", "Assigns has an @.", "\r\nThis is the message: ",
 "Hello from the view!"]

A Word About Layouts

レイアウトも普通のテンプレートと同じ事はControllersの章で学びました。
じゃあどうやって各々のビューでこのレイアウトを取り込んでるんでしょうか?
lib/hello_phoenix_web/templates/layout/app.html.eexを見てみます。

...
<%= render @view_module, @view_template, assigns %>
...

この箇所で他のビューを挿入しています。カスタマイズするときはこの場所を変えたりするんでしょうか。

今回も少々長くなってるので区切ります。
多分次の記事でビューは終わるはず

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

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。 Controllersの続きです。今回で終われるかな?

Redirection

Phoenix上でのリダイレクトの方法のようです。
アイテムを作成した後に表示画面にリダイレクトしたいときなんかに使えそうです。例えばlocalhost:400/create/1234にPOSTした後にlocalhost:400/show/1234にリダイレクトする感じでしょうか。
内部のパスにリダイレクトする際と外部にリダイレクトする際(例えばいかがわしいページで18歳以上ですか?→いいえ→Yahoo、などでしょうか)で少々指定の方法が違うようです。
まずは内部の例から。lib/hello_phoenix_web/router.exを編集します。

defmodule HelloPhoenixWeb.Router do
  use HelloPhoenixWeb, :router
  ...

  scope "/", HelloPhoenixWebdo
    ...
    get "/", PageController, :index
  end

  # New route for redirects
  scope "/", HelloPhoenixWebdo
    get "/redirect_test", PageController, :redirect_test, as: :redirect_test
  end
  ...
end

お次にlib/hello_phoenix_web/controllers/page_controller.exを編集します。

defmodule HelloPhoenixWeb.PageController do
  use HelloPhoenixWeb, :controller

  def index(conn, _params) do
    # render(conn, "index.html")
    redirect(conn, to: "/redirect_test")
  end

  def redirect_test(conn, _params) do
    text(conn, "Redirect!")
  end
end

どうやらlocalhost:4000/にアクセスしたらlocalhost:4000/redirect_testに飛ぶように設定しているようです。
リダイレクト先の表示はプレーンテキストになっています。
本来であればレンダリング関数が書かれているところにredirect/2関数を記載し、第二引数にto: "リダイレクト先のパス"という書き方のようです。

リダイレクトされている状況は開発者ツールなどで閲覧できます。Chromeの場合はCtrl + Shift + IででるウィンドウのNetworkというタブで確認可能です。

f:id:ysmn_deus:20190308181914p:plain

302ステータスになっています。

外部にリダイレクトするときはexternal: "外部URL"のようです。

def index(conn, _params) do
  redirect(conn, external: "https://elixir-lang.org/")
end

ルーティングの章で学んだPath helperを利用してのリダイレクトも可能なようです。

  def index(conn, _params) do
    redirect(conn, to: Routes.redirect_test_path(conn, :redirect_test)) # redirect_test_urlではない、to: の対象は”パス”
  end

個人的にはハードコードを極力避けたいのでこっちの方が安心感はあるかな?とは思います。

Action Fallback

アクションのエラーを毎回書くのは面倒です。ここではブログの記事を取ってくるURLにリクエストが来たときのエラー処理を想定しています。
例えば、そもそも存在しない記事を参照しようとした場合、404でNotFoundの処理をするでしょう。もしくは特定の人しか閲覧できないような記事の場合、認証に失敗して403を返す時もあるでしょう。
こういう処理をアクション毎に書いていくのは冗長でElixirっぽくないです。やはりこういう処理を回避する方法が用意されています。

ここでの例は上記でも申し上げた通り、ブログの記事を取ってくるアクションを想定します。
Blog.fetch_post/1という関数でidpost情報を取得し、Authorizer.authorize/3でそのポスト情報が現在のユーザーで閲覧可能かどうかチェックしています。
Blog.fetch_post/1で失敗すると{:error, :not_found}が、Authorizer.authorize/3で失敗すると{:error, :unauthorized}が返ってきます。

defmodule HelloPhoenixWeb.MyController do
  use Phoenix.Controller
  alias HelloPhoenix.{Authorizer, Blog}
  alias HelloPhoenix.ErrorView

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- Blog.fetch_post(id),
         :ok <- Authorizer.authorize(current_user, :view, post) do

      render(conn, "show.json", post: post)
    else
      {:error, :not_found} ->
        conn
        |> put_status(:not_found)
        |> put_view(ErrorView)
        |> render(:"404")
      {:error, :unauthorized} ->
        conn
        |> put_status(403)
        |> put_view(ErrorView)
        |> render(:"403")
    end
  end
end

一箇所だけなら必要な処理なので仕方ないとは思うのですが、別のアクションでも同様の処理を書いていくとなると保守性にも優れませんのでこのエラー処理はどこかで一元管理したいところです。
そこで別にコントローラを作成してそれを再利用することにします。

defmodule HelloPhoenixWeb.MyFallbackController do
  use Phoenix.Controller
  alias HelloPhoenixWeb.ErrorView

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(ErrorView)
    |> render(:"404")
  end

  def call(conn, {:error, :unauthorized}) do
    conn
    |> put_status(403)
    |> put_view(ErrorView)
    |> render(:"403")
  end
end

これを必要に応じてコントローラでaction_fallbackマクロで読み込みます。

defmodule HelloPhoenixWeb.MyController do
  use Phoenix.Controller
  alias HelloPhoenix.{Authorizer, Blog}

  action_fallback HelloPhoenixWeb.MyFallbackController

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- Blog.fetch_post(id),
         :ok <- Authorizer.authorize(current_user, :view, post) do

      render(conn, "show.json", post: post)
    end
  end
end

かなりスッキリしました!
これもコントローラがPlugである恩恵でしょう、action_fallbackも内部はPlugで実装されているようです。

Halting the Plug Pipeline

上記でもPlugの有用性に関心しましたが、コントローラはPlugで連なっている構造になっています。
エラーが起こるなど、時と場合によってはPlugの処理中に後の処理をする必要がなくPlugの処理を止めたい時があるとおもいます。そういうときの為にPlug.Conn.halt/1という関数が用意されているようです。

ここではHelloPhoenixWeb.PostFinderというPlugを想定します。これはブログなどの投稿を探してきてconnassignしてくれる(接続情報にポスト情報を載っけてくれる)Plugを想定しています。
もし見つからない場合は404ページを返します。

defmodule HelloPhoenixWeb.PostFinder do
  use Plug
  import Plug.Conn

  alias HelloPhoenix.Blog

  def init(opts), do: opts

  def call(conn, _) do
    case Blog.get_post(conn.params["id"]) do
      {:ok, post} ->
        assign(conn, :post, post)
      {:error, :notfound} ->
        conn
        |> send_resp(404, "Not found")
    end
  end
end

上記の例では404のレスポンスを返しても後になにがしかのPlugが連なっている場合は処理が走るようです。(レンダリングされる所には反映されないかもしれませんが、無駄な処理です。)
そこで、404のケースにはhalt/1関数を入れます。

    ...
    case Blog.get_post(conn.params["id"]) do
      {:ok, post} ->
        assign(conn, :post, post)
      {:error, :notfound} ->
        conn
        |> send_resp(404, "Not found")
        |> halt()
    end

halt/1関数は内部的にはPlug.Conn.tの値をtrueにするだけの関数のようです。
全てのPlugはこのPlug.Conn.ttrueである場合はただ接続情報を渡すだけの存在になり果てるようです。

def post_authorization_plug(%{halted: true} = conn, _), do: conn
def post_authorization_plug(conn, _) do
  ...
end

また、このhalt/1関数はPlug.Conn.ttrueにするだけなので、実際呼び出されたPlugの処理は止まりません。次のPlug以降に影響します。
なので

conn
|> send_resp(404, "Not found")
|> halt()

と書こうが

conn
|> halt()
|> send_resp(404, "Not found")

と書こうが、処理は同じになるようです。

ようやくControllersが終わりました。
Controllersの章を総括しても、やはりいかにPlugの概念を理解し応用するかがPhoenixでの生産性を高める結果に繋がりそうです。

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以降はまた次回!

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

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。 今回はControllersということで、コントローラの話です。
ルーティングのところで一通りは把握している所ですが、少々深掘りしていきましょう。

今のところ、Phoenixの作業は

  1. mix phx.new project_nameでプロジェクトを作成
  2. データベース関連の操作(データベース用意したり設定書いたりmix ecto.createしたり)
  3. router.exを編集してルーティングを調整
  4. controllersにコントローラを追加
  5. viewsにビューを追加
  6. 必要であればtemplatesにテンプレートを追加(JSONならたぶんいらない)
  7. 3~6を繰り返し

みたいな流れになりそうです。
コントローラは機能としてはルーティングとレンダリングの橋渡しをする箇所を担っています。

Phoenixプロジェクトを開始した際に入っているlib/hello_web/controllers/page_controller.exには下記のように書かれています。

defmodule HelloPhoenixWeb.PageController do
  use HelloPhoenixWeb, :controller

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

コントローラを追加していく時はコレをベースに作成していくのがよさそうです。

また、コントローラもPlugで実装されているようです。

Actions

コントローラでActionと呼称されているものがありますが、ただの関数です。アクションと関数をわざわざ言い換える必要も無いのですが、アクション関数の目的がレンダリングすることであり、「○○モジュールのレンダリングする関数」と言うのがめんどいのでアクションと呼んでいるに過ぎないのでしょう。
PageControllerにデフォルトで備わっているアクションは:indexで、ルーティングファイルから下記のように呼び出されています。

get "/", PageController, :index

テスト用には:testというアクションが想定されているようです。

get "/", PageController, :test

コントローラにテストを追加するならこんなかんじ

defmodule HelloPhoenixWeb.PageController do
  use HelloPhoenixWeb, :controller
  ...

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

まぁ、現段階ではテストになってないんですが。

アクションの命名自体は自由だそうですが、ルーティングの章でResourcesをやりました。これを鑑みると下記のアクション名は機能をResourcesに揃えておくのが望ましいでしょう。

  • index
    • リソースのアイテムを全表示する
  • show
    • IDで識別されるアイテムを表示する
  • new
    • 新しいアイテムを作成する画面を表示する
  • create
    • 新しいアイテムを作成する変数を受けてデータベースなどに保存する(表示自体はリダイレクト?)
  • edit
    • 編集するIDの変数を取り出す+編集できるフォームを表示する
  • update
    • 編集するアイテムの変数を受けてデータベースなどに上書き保存する
  • delete
    • 削除するIDを受けてデータベースから削除する(確認画面なんかも出した方がいい?)

アクションは基本的に2つ引数をとります。1個目はconnという事でPlugのところで出てきている接続情報(%Plug.Conn{})です。
これはコントローラがPlugで実装されているからでしょう。

2個目の引数はparamsで、必要に応じてHTTPリクエストからパラメータを取り出すマップとなっています。
Adding Pagesの章でやりましたが、ルーティングの際に

get "/hello/:messenger", HelloController, :show

と書かれている場合はリクエストが来た場合にparams"messenger"というキーで値が入っています。
例えばhttp://localhost:4000/hello/hogefugaというアドレスにアクセスがあった場合には、paramsには"messenger"というキーに"hogefuga"が値として渡されます。
これをコントローラで

defmodule HelloPhoenixWeb.PageController do
  ...

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

というふうに、パターンマッチングで受けるのがPhoenixの習わしっぽいです。

:indexアクションの様にパラメータが不要な際は_paramsと”関数の実行に関わりの無い引数”としておくのが良いでしょう。(Elixirのならわし)

Gathering Data

データのストア(記録、読み出しなど)機能はPhoenixの機能としては実装してないようです。
それもそのはず、EctoというデータベースラッパーがElixirには存在しているらしく、そちらを使うと良いよ!と記載があります。
なので、基本的には

  1. リクエストが来てコントローラのアクションが呼び出される
  2. アクション内でEctoやETS、OTPで作ったデータストアでデータを呼び出す
  3. レンダリング関数を呼び出してビューモジュールで処理

という流れになると思います。
実際に開発を進めるに当たってEctoも重要になってきそうですので、また別途記事にしたいと思います。

hexdocs.pm

Flash Messages

Flash Messagesという機能があるようです。フロントでもちょいちょい見たような?
SPAなどでよくある「ログインに失敗しました」みたいなのがぴょこっと出てきて画面遷移しない時とかに使う奴だと思います。

コントローラを使える状態であればput_flash/3get_flash/2でメッセージのやりとりができるようです。サンプルを見てみます。

defmodule HelloPhoenixWeb.PageController do
  ...
  def index(conn, _params) do
    conn
    |> put_flash(:info, "Welcome to Phoenix, from flash info!")
    |> put_flash(:error, "Let's pretend we have an error.")
    |> render("index.html")
  end
end

put_flash/3の第一引数はパイプラインですので接続情報、第二引数はメッセージのキー、第三引数はメッセージのバリューという関数でしょう。
特にキー名などの制約はないようですが、:info:errorはよく使われるようです。

表示するときはテンプレートでget_flash/2を使うようです。

<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>

第一引数が接続情報、第二引数がキーというかんじです。
他にもclear_flash/1という関数もあるようですが、このへんはFlash Messagesを使いたくなったら検討しましょう。
なんとなくフロント側で処理する方がスッキリする気がします。

Controllersちょっと長いので一旦ここで区切ります。

Phoenix入門 (第6章 Endpoint)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。 今回はEndpointということでPlugのところでもちょっとやったところです。
全てのリクエストはまずEndpointに渡されて、その後にルーティング処理に入ります。

そもそもPhoenixは一番最初にEndpointをSupervisorの管理下に起動するようです。
lib/hello_phoenix/application.exを確認します。

defmodule HelloPhoenix.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      # Start the Ecto repository
      HelloPhoenix.Repo,
      # Start the endpoint when the application starts
      HelloPhoenixWeb.Endpoint
      # Starts a worker by calling: HelloPhoenix.Worker.start_link(arg)
      # {HelloPhoenix.Worker, arg},
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: HelloPhoenix.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  def config_change(changed, _new, removed) do
    HelloPhoenixWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end

Endpointがchildrenに入っており、Supervisor.start_link(children, opts)が実行されています。

Endpoint Contents

Phoenixのプロジェクトを開始した際に自動で生成されるlib/hello_phoenix_web/endpoint.exの中を見ていきます。

defmodule HelloPhoenixWeb.Endpoint do
  ...
end

まず最初に

use Phoenix.Endpoint, otp_app: :hello

とあります。PhoenixのEndpointとして利用するためにuseで様々な機能を呼び出しています。
otp_app: :helloは設定用に用いられると記載がありますので、おそらくconfig/config.exsに記載がある

# Configures the endpoint
config :hello_phoenix, HelloPhoenixWeb.Endpoint,
...

の箇所の設定などが適応されるのでしょう。

次はSocketの設定。

  socket "/socket", HelloPhoenixWeb.UserSocket,
    websocket: true,
    longpoll: false

RoutingのChannelの箇所でも見ましたが、websocketの設定だと思います。
詳しくはChannelsの章に回します。

次はPlug.Staticが適応されています。名前からしても静的ファイルの配信などでしょう。

  plug Plug.Static,
    at: "/",
    from: :hello_phoenix,
    gzip: false,
    only: ~w(css fonts images js favicon.ico robots.txt)

圧縮を適応したり配信ファイルを設定したりフィルタリング(該当する静的ファイルのリクエストに対してのみ圧縮ファイルを利用するとか?)の設定とかできそうです。

次はコードリロードが適応されてるかどうかの処理。

  if code_reloading? do
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
    plug Phoenix.LiveReloader
    plug Phoenix.CodeReloader
  end

コードリロードはwebsocketで実装されてるんですかね。
主にホットリロードの機能は開発時のみに適応されます。ホットリロードのON/OFFはconfig/config.exsのEndpointの箇所で設定するようです。

config :hello_phoenix, HelloPhoenixWeb.Endpoint, code_reloader: true

お次はPlug.RequestIdPlug.Logger。以前にちょっとだけ言及した気はしますがPlug.RequestIdが各リクエストに対してユニークなIDを降るPlugで、Plug.Loggerはロギングでしょう。
より詳しく知るにはPlugのModulesを閲覧するのが良さそうです。今はスルーします。

次は公式ドキュメントでの言及はありませんでしたが、Plug.ParsersPlug.MethodOverridePlug.Head。これもルーティングのところで言及していますが、 Plug.Parsersがリクエストの本体をパースするPlug、 Plug.MethodOverrideがmethodのパラメータからPOSTのリクエストをPUT、PATCH、DELETEに書き換えるPlug、 Plug.HeadがHEADリクエストをGETリクエストに変換するPlugです。

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()

  plug Plug.MethodOverride
  plug Plug.Head

お次にPlug.Session。名前の通りCookiesなどセッション情報の処理をするPlugでしょう。

  plug Plug.Session,
    store: :cookie,
    key: "_hello_phoenix_key",
    signing_salt: "hogefuga"

最後にルーティング機能のPlugを呼び出しています。同じディレクトリにあるrouter.exが該当します。

  plug HelloPhoenixWeb.Router

もっと色々知りたい人はEndpoint API docsを読むといいよ!と書かれています。今回はスルーしますが、必要に応じて確認したいところです。

hexdocs.pm

Using SSL

SSLを使うときには設定を書き換えるのと2つの環境変数が必要なようです。
設定はconfig/prod.exsに書き、otp_app: :hello_phoenixを指定しているときは秘密鍵と証明書はpriv/ディレクトリに入れとくようです。

use Mix.Config

config :hello_phoenix, HelloPhoenixWeb.Endpoint,
  http: [port: {:system, "PORT"}],
  url: [host: "example.com"],
  cache_static_manifest: "priv/static/cache_manifest.json",
  https: [
    port: 443,
    otp_app: :hello_phoenix,
    keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
    certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
    # OPTIONAL Key for intermediate certificates:
    cacertfile: System.get_env("INTERMEDIATE_CERTFILE_PATH")
  ]

otp_app:の値なしでの設定方法も書いてますが、ちょっとよく分からないのでスルーします。(PlugでPath.expand("../../../some/path/to/ssl/key.pem", __DIR__)が必要って書いてあるけどどのPlug?)

TLSのバージョンなど厳密に指定したい場合はversions: [:'tlsv1.2']などのキーを追加すればいいようですが、セキュリティ上望ましくないと思いますし気にしなくて良い気がします。

SSL in Development

開発時にSSLを使いたい時はmix phx.gen.certオレオレ証明書(self-signed certificate)を発行できるようです。
開発時のみでの使用が推奨されますので、config/dev.exsに記載します。

config :hello_phoenix, HelloPhoenixWeb.Endpoint,
  ...
  https: [
    port: 4001,
    cipher_suite: :strong,
    keyfile: "priv/cert/selfsigned_key.pem",
    certfile: "priv/cert/selfsigned.pem"
  ]

Force SSL

SSLのページにリダイレクトしたいときとかの設定?
Endpointのコンフィグにforce_ssl:を追記します。
:urlの設定に書いてねと言われてますのでconfig/prod.exsとかでしょうか。若しくは開発時に試すためにconfig/dev.exs

config :hello_phoenix, HelloPhoenixWeb.Endpoint,
  force_ssl: [rewrite_on: [:x_forwarded_proto]]

ダイナミックにリダイレクト(たぶんhttp://hoge.com/api/fugahttps://hoge.com/api/fugaとか?)したい場合はhost: nilを設定するようです。

config :my_app, MyApp.Endpoint,
  force_ssl: [rewrite_on: [:x_forwarded_proto], host: nil]

HSTS

HTTP Strict Transport Securityを適応したい場合はforce_ssl: :hstsと書けば適応されるそうです。
ただし、ローカル環境での開発だとなんかしらんけど動かん、とかあるみたい(80への通信を強制的に443にリダイレクトするあたりが悪さする?)なんで、ブラウザごとで色々対策してねとあります。この辺を鑑みると開発用サーバーにデプロイするなど、HSTSを使う際にはあまりローカルで開発しない方がいいかもしれません。

SSLあたりはまだまだ勉強不足なので実際使ってみてまた何かあれば補足情報など記載したいと思います。

Phoenix入門 (第5章 Plug)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
お次はPlug。というかPhoenixの説明でPlugPlug言われてたので先にこの章からやれば良かった。
今まで結構でてきてるところから分かるとおり、PlugはPhoenixのコアな部分です。PhoenixのHTTPレイヤーなどを担当しているっぽいのでもうちょっと理解しておきたいです。

EndpointsやRouters、Controllersは内部的にはPlugになってるそうです。
一応番外編としてPlugのドキュメンテーションを触っていますが、そのときはCowboy?を利用してウェブサーバーを立ち上げていました。
ヘッダーの情報などを付与したり、ルーティングしたりという役割を果たしていましたが、Plugの基本的な発想は私たちがハンドリングする接続のコンセプトを統一する、というものだそうです。おそらくヘッダー付けたりルーティングしたりってことを指してるんでしょうが、よくわかんないので先に進みます。

Function Plugs

これはPlugの番外編でも出てた気がしますが、関数のプラグです。
コネクションの構造(マップ、いわゆる%Plug.Conn{})とオプションを貰うだけの単純な関数で表されます。

def put_headers(conn, key_values) do
  Enum.reduce key_values, conn, fn {k, v}, conn ->
    Plug.Conn.put_resp_header(conn, to_string(k), v)
  end
end

key_valuesの各要素毎にPlug.Conn.put_resp_header(conn, to_string(k), v)が実行され、ヘッダー情報が追加される関数です。
この関数を定義しておけば、Phoenixでは下記のように取り扱えるようです。

defmodule HelloWeb.MessageController do
  use HelloWeb, :controller

  plug :put_headers, %{content_encoding: "gzip", cache_control: "max-age=3600"}
  plug :put_layout, "bare.html"

  ...
end

なるほど、なんとなく理解できてきました。
plug :hogehogehogehogeという関数プラグをリクエストに対して実行して次のplug :fugafugaに渡していく、という感じでしょうか。 このplugという仕組みはWebリクエストに対する処理を徹底的にシンプルにする仕組みのようです。

認証処理のリダイレクトなどのエラー処理をPlugを用いないで書くとどうなるか、丁寧にもサンプルを載せてくれています。

defmodule HelloWeb.MessageController do
  use HelloWeb, :controller

  def show(conn, params) do
    case authenticate(conn) do
      {:ok, user} ->
        case find_message(params["id"]) do
          nil ->
            conn |> put_flash(:info, "That message wasn't found") |> redirect(to: "/")
          message ->
            case authorize_message(conn, params["id"]) do
              :ok ->
                render(conn, :show, page: find_message(params["id"]))
              :error ->
                conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/")
            end
        end
      :error ->
        conn |> put_flash(:info, "You must be logged in") |> redirect(to: "/")
    end
  end
end

つまりElixirでこういう不格好なコードは書くなよ?という威圧感を感じます。ああこわい・・・
実際こういうコードを書いていてはコーディングにミスが多発しそうです。

Plugは認証処理なんかもシンプルにしてくれます。上記のネストだらけのコードをPlugを使ってシンプルにします。

defmodule HelloWeb.MessageController do
  use HelloWeb, :controller

  plug :authenticate
  plug :fetch_message
  plug :authorize_message

  def show(conn, params) do
    render(conn, :show, page: find_message(params["id"]))
  end

  defp authenticate(conn, _) do
    case Authenticator.find_user(conn) do
      {:ok, user} ->
        assign(conn, :user, user)
      :error ->
        conn |> put_flash(:info, "You must be logged in") |> redirect(to: "/") |> halt()
    end
  end

  defp fetch_message(conn, _) do
    case find_message(conn.params["id"]) do
      nil ->
        conn |> put_flash(:info, "That message wasn't found") |> redirect(to: "/") |> halt()
      message ->
        assign(conn, :message, message)
    end
  end

  defp authorize_message(conn, _) do
    if Authorizer.can_access?(conn.assigns[:user], conn.assigns[:message]) do
      conn
    else
      conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/") |> halt()
    end
  end
end

かなりスッキリしました。
これだけでもPlugという方法でWebリクエストを処理するメリットを感じます。

Module Plugs

上記の関数プラグに加えてモジュールでもPlugを定義できます。

defmodule HelloWeb.Plugs.Locale do
  import Plug.Conn

  @locales ["en", "fr", "de"]

  def init(default), do: default

  def call(%Plug.Conn{params: %{"locale" => loc}} = conn, _default) when loc in @locales do
    assign(conn, :locale, loc)
  end
  def call(conn, default), do: assign(conn, :locale, default)
end

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug HelloWeb.Plugs.Locale, "en"
  end
  ...

モジュールプラグとして利用するモジュールでは、init/1call/2というコールバック関数を定義する必要があります。
init/1は文字通り初期化用のコールバック関数でplug HelloWeb.Plugs.Locale, "en"の箇所で呼び出されているようです。今回ではdefaultという変数に"en"を代入する役割を果たしています。
call/2はリクエストの度に呼び出される関数で、第一引数に接続情報、第二引数にオプションを指定する関数のようです。

今回のモジュールプラグはcall/2関数でパターンマッチングで条件分岐しており、モジュールが抱えているlocalesというリストの中に通過するリクエストのパラメータ内のlocaleが該当するか否かを判定しています。ある場合はそのlocaleを、無ければデフォルトの"en"アサインするような処理になっています。
内部にモジュール属性で変数を持たせて条件分岐したいときなどはモジュールプラグの方がいいのかもしれません。

リクエストに対して毎回行う処理などはPlugを駆使するのがPhoenixの流儀っぽいです。
若干まだ慣れてない感はありますが、ウェブ上で公開されているコードなども参考にしてどう扱うか吟味していきたいところです。