ConnからSocketにコンバートする方法+phx.gen.authのログイン処理に関するメモ
Socketは生成されるときに、Connをもとに生成される。
具体的にどのように記述するべきなのか調べたメモ。
ConnとSocketとは
Plug.ConnはHTTPリクエストを受信したときに生成される構造体である。
リクエストとレスポンスに用いられ、リクエスト用のフィールドと
レスポンス用のフィールド、またPhoenix側で値を格納するフィールドを含んでいる。
またPlug.Conn(モジュール)自体はリクエストとレスポンスを操作するためのモジュールであり、後述するPlug.Sessionにアクセスするためにも使用する。
Phoenix.LiveView.SocketはliveルートにHTTPリクエストを受信したときに生成される構造体である。
これが状態と呼ばれるものであり、ライフサイクル上で必要なデータを格納している。
Plug.Sessionはルートに到達したときに生成される構造体である。セッション管理に使用される。
コンバートの方法
このような場合、まずlive_sessionというマクロでliveセッションを作成する必要がある。
liveセッションとは、いくつかのliveルートをグループ化するものである
このマクロにはon_mountというオプションがある。
on_mountコールバックを実装したモジュールと第一引数のアトムを、次の形式で指定する。(パターンマッチで処理を分岐させている)
よってlive_sessionマクロは次の形式で記述する。
live_session セッション名,
on_mount: [{モジュール, アトム}] do
ルート
...
end
liveセッション内で必要となる共通のデータを状態に追加する処理などを実行することができる。
live_sessionは次のようにscope内に定義することができる。
live_session :return_to_home_live_index_required_authentication,
on_mount: [{SampleWeb.AccountAuth, :ensure_authenticated}] do
live "/", HomeLive.Index, :index
end
なお、liveセッション名は理解しやすい名前にすること。
on_mountの実装
on_mountの実装は次の形式で記述する。
def on_mount(アトム, params, session, socket) do
...
end
戻り値は、次のタプルとなる。
-
{:cont, socket}- このままsocketを渡し、mountを開始する。 -
{:halt, socket}- mountを中止する。リダイレクトを行う場合はこちらを返すようにする。
実装は例のようになる。
def on_mount(アトム, _params, _session, socket) do
case :takashi do
:takashi -> {:cont, assign(socket, :name, "takashi")}
:tanuki -> {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/")}
end
end
データの受け渡し
connのデータをsocketに渡すためには、pipe_through(scopeの固有の処理)でPlug.sessionへの値の挿入を行う必要がある。
次のput_takashiという処理でデータの受け渡しを行うものとする。
pipe_through [:browser, put_takashi]
以下のように定義されている。
def put_takashi(conn, _opts) do
put_session(conn, :takashi, "takashi")
end
処理が実行されると、Plug.Connのprivateフィールドにある:plug_sessionというキーの値であるmapにtakashi: "takashi"が挿入される。
private: %{ :plug_session => %{..., takashi: "takashi"}, ...}
この:plug_sessionの値はLiveViewのmount第2引数で参照することができる。
def mount(_params, session, socket) do
IO.puts "session: #{inspect session}"
...
end
IO.putsの出力は以下のようになる。
session: %{..., "takashi" => "takashi"}
sessionに値を渡すことができたので、次はassignsに渡す。
sessionから値を取得するだけ。
def on_mount(アトム, _params, session, socket) do
{:cont, assign(socket, :name, session[:takashi])
end
結論
ルートにリクエストを受信する前にsocketにデータを追加したい場合、次の手順を実行する。
-
scopeにlive_sessionマクロの記述をする。 - liveセッション名と実行する
on_mountを{モジュール名, アトム}で指定する。 -
on_mountコールバックを実装して、on_mount:に指定する。 -
do .. endにルートをliveルートを記述する。
phx.gen.authのログイン処理に関するメモ
以下のスコープがあるとする。
scope "/", SampleWeb do
pipe_through [:browser, :require_authenticated_account]
live_session :require_authenticated_account,
on_mount: [{SampleWeb.AccountAuth, :ensure_authenticated}] do
live "/accounts/settings", AccountSettingsLive, :edit
live "/accounts/settings/confirm_email/:token", AccountSettingsLive, :confirm_email
end
end
スコープ内に記述されていることを要約すると次のようになる。
- このスコープは内のliveルートはログインを必要とする。ログインしていない場合、ログインページにリダイレクトする。
-
live_sessionマクロに記述しているliveルートは/accounts/settings/以下略はログインを必要とする。ログインしていないとき、ログインページにリダイレクトする。
疑問に思うことは次のようなことである。
「どちらもログインしていない場合はログインページにリダイレクトするので、require_authenticated_accountを実行しない場合、どのような動作をするのか」
調査結果
require_authenticated_accountの処理の内容は、conn.assigns[:current_account]の存在するかどうかにより次の動作を行う。
存在する場合:
何もしない
存在しない場合:
flashメッセージを表示して、現在のパスをセッションに追加し、ログインページにリダイレクト。
on_mount(:ensure_authenticated, ...)の処理の内容は、session.assigns.current_accountの存在するかどうかにより次の動作を行う。
存在する場合:
socketに追加するだけ。
存在しない場合:
flashメッセージを表示して、ログインページにリダイレクト。
大きく異なる点は、ログイン後にリダイレクト前のページに戻れるかどうかである。
つまり、該当ルートに到達したときの処理は次のようになる。
- ログインしていないので
require_authenticated_accountでログインページにリダイレクト
- このとき
maybe_store_return_to(%{method: "GET"} = conn)によってsessionの:account_return_toというキーの値に現在のパスが格納される
- ログインページで入力を行う
- 入力(構造体のインスタンス)が
AccountSessionController, :createのparamsに渡される
- 実行されるアクション内で
AccountAuth.log_in_accountが呼び出される
- sessionを初期化した後、予め取得した
:account_return_toの値(リダイレクト前パス)にリダイレクト(or/に飛ぶ)
よって:require_authenticated_accounはログイン後にもとのページに戻るための処理であるので、実行しない場合はログイン後に/にリダイレクトする(signed_in_path()に記述されている)。
なお、connにcurrent_accountを追加する処理は、pipelineに追加されているfetch_current_accountによるものである。
以下のような記述は、on_mount側でログインを確認するまでもないので、on_mount(:mount_current_account)を実行しても良いということになる。
pipe_through [:browser, :require_authenticated_account]
...
on_mount: [{SampleWeb.AccountAuth, :mount_current_account}] do
...
end