今日は以下あたりを機械翻訳したのでそれを見つつポイント列挙の方向。
- Plug は Phoenix の HTTP レイヤの中心におり、Phoenix は Plug を front と center に配置する
- connection lifecycle の全ての step で Plug と対話
- Endpoint、Router、Controller などの Phoenix の中核的なコンポーネントはすべて内部的に Plug
- Plug は Web アプリケーション間の構成可能なモジュールの仕様
- 異る Web サーバの接続アダプタの抽象化レイヤでもある
- Plug の基本的な考えかたは我々が操作する “接続” の概念を統一すること
- 要求と応答がミドルウェアスタックで分離されている Rack などの他の HTTP ミドルウェアレイヤとは異なる
- 最も単純なレベルでは Plug の仕様には二つの種類がある、function plugs と module plugs
Function Plugs
- Plug として機能するためには手続きは接続構造体 (%Plug.Conn{}) とオプションを受け取る必要がある
- 接続構造体も戻す必要がある
- これらの基準を満たす機能はすべて実行される
以下は pub_headers の例。
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
以下はこれを使う方法。
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 が定義されていたとして
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
以下なコントローラの実装で
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
Plug が (例えば authenticate) :error の状態となった場合、halt()
が呼び出されて以降の Plug は実行されない模様。これは良いですね。
Module Plugs
- Module Plug はモジュール内で接続変換を定義できるようにするもう一つのタイプの Plug
- 二つの機能を実装する必要がある
call/2
に渡す引数やオプションを初期化するinit/1
- 接続変換を実行する
call/2
は先に見た function plug
以下が Module 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
...
引数のパタンマッチ使っています。Elixir では =
はパタンマッチなので最初の call は
- @locales にあるいずれかの要素を “locale” な params として持っている conn
であれば呼び出されてそれ以外なら下の call が呼び出される、という理解で良いのかな。