どうも、靖宗です。 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
というタブで確認可能です。
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
という関数でid
のpost
情報を取得し、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を想定します。これはブログなどの投稿を探してきてconn
にassign
してくれる(接続情報にポスト情報を載っけてくれる)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.t
がtrue
である場合はただ接続情報を渡すだけの存在になり果てるようです。
def post_authorization_plug(%{halted: true} = conn, _), do: conn def post_authorization_plug(conn, _) do ... end
また、このhalt/1
関数はPlug.Conn.t
をtrue
にするだけなので、実際呼び出されたPlugの処理は止まりません。次のPlug以降に影響します。
なので
conn |> send_resp(404, "Not found") |> halt()
と書こうが
conn |> halt() |> send_resp(404, "Not found")
と書こうが、処理は同じになるようです。
ようやくControllersが終わりました。
Controllersの章を総括しても、やはりいかにPlugの概念を理解し応用するかがPhoenixでの生産性を高める結果に繋がりそうです。