どうも、靖宗です。
お次は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 :hogehoge
でhogehoge
という関数プラグをリクエストに対して実行して次の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/1
とcall/2
というコールバック関数を定義する必要があります。
init/1
は文字通り初期化用のコールバック関数でplug HelloWeb.Plugs.Locale, "en"
の箇所で呼び出されているようです。今回ではdefault
という変数に"en"
を代入する役割を果たしています。
call/2
はリクエストの度に呼び出される関数で、第一引数に接続情報、第二引数にオプションを指定する関数のようです。
今回のモジュールプラグはcall/2
関数でパターンマッチングで条件分岐しており、モジュールが抱えているlocales
というリストの中に通過するリクエストのパラメータ内のlocale
が該当するか否かを判定しています。ある場合はそのlocale
を、無ければデフォルトの"en"
をアサインするような処理になっています。
内部にモジュール属性で変数を持たせて条件分岐したいときなどはモジュールプラグの方がいいのかもしれません。
リクエストに対して毎回行う処理などはPlugを駆使するのがPhoenixの流儀っぽいです。
若干まだ慣れてない感はありますが、ウェブ上で公開されているコードなども参考にしてどう扱うか吟味していきたいところです。