經由 Endpoint 的 pipeline 處理,並依 URI 進行分派之後,connection 將會傳到的 Controller 的個別函式中。我們把這些函式稱為 action,因為他們分別對應了 CRUD 的其中一個行為。
試著把 scope 及 resource 的結果展開一條來當範例,當我們在 Router 中寫了這行 :
get "/post", PostController, :index
在 mix phx.routes
就會顯示:
post_path GET /posts HelloPhxWeb.PostController :index
而符合這個 URI 的 HTTP 請求,在通過 Endpoint 之後,就會將 connection 傳到 PostController
的 index
函式。
與 Rails 倡導的 fat model, thin controller 不同,在 Phoenix Controller 的每個 action 裡,會主動調用各個 Context 的取值與寫值函式,並在蒐集完資料後,算繪 (render) 出最終的 HTML 或是 JSON 回應,存入 connection 後回傳。簡而言之,Phoenix 的 Controller 是負責存取資料及分發的流程控制中心。
進行到這一篇,當我說 Phoenix 的 Controller 其實也是個 plug 時,你應該已經不會覺得太訝異了。
Controller 裡的每個 action 函式,也符合 plug 的規範,接收兩個參數並回傳一個新的 connection。第一個參數當然是我們反覆提到的 connection,%Plug.Conn{}
。而第二個參數,則是 HTTP 請求時所帶上的參數 (雖然這些值在 connection 裡也翻得到),會以一個用字串當 key 的 Map
傳入。
我們來看一下 PostController
裡的幾個重點函式。
def index(conn, _params) do
posts = Blog.list_posts()
render(conn, "index.html", posts: posts)
end
由於用不到 URI 傳入的參數 (根本沒有什麼參數吧),用上了 _params
表示出我們的不在乎。在函式本體的第一行,調用了 Blog
context 的 list_posts
取得 post 清單的資料。接著用 connection、template 名稱 "index.html"
及取得的清單做為參數,呼叫 render
算繪 HTML 內容並回傳。
def new(conn, _params) do
changeset = Blog.change_post(%Post{})
render(conn, "new.html", changeset: changeset)
end
new 用來顯示想新增 post 時要填的空白表格,因此我們也不在乎傳入的 URI 參數。在用 Blog.change_post(%Post{})
做出一個空白的 changeset* 之後,以 connection,template 名稱 "new.html"
及 changeset 做為參數呼叫 render/3
來算繪顯示的表格頁面。
def create(conn, %{"post" => post_params}) do
case Blog.create_post(post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post created successfully.")
|> redirect(to: post_path(conn, :show, post))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
這個 action 需要將新的 post 資料寫入資料庫中。參數裡的 post_params
會給我們已經整理好的資料,所以這裡調用了 Blog.create_post
來保存資料。
我們用 case
來比對保存行為的結果是成功的 {:ok, post}
或是失敗的 {:error, changeset}
。如果是成功的話,用 put_flash
放入新增成功的提示訊息,並重導到已建好的 post 頁面。如果是失敗的情況,則用帶有錯誤訊息的 changeset 資料來重新顯示新增 post 表格頁面。
def edit(conn, %{"id" => id}) do
post = Blog.get_post!(id)
changeset = Blog.change_post(post)
render(conn, "edit.html", post: post, changeset: changeset)
end
def update(conn, %{"id" => id, "post" => post_params}) do
post = Blog.get_post!(id)
case Blog.update_post(post, post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post updated successfully.")
|> redirect(to: post_path(conn, :show, post))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", post: post, changeset: changeset)
end
end
概念與 new -> create 相同,是用來更新已保存的資料。所以當顯示 edit 表格時,我們需要想更正的資料的 id 做為參數,並取出資料庫相應的記錄來顯示表格。而 update 則與 create 相仿,試著將將自表格頁面傳來的新內容寫入資料庫中。
def delete(conn, %{"id" => id}) do
post = Blog.get_post!(id)
{:ok, _post} = Blog.delete_post(post)
conn
|> put_flash(:info, "Post deleted successfully.")
|> redirect(to: post_path(conn, :index))
end
刪除 post 就只需要該筆記錄的 id,成功之後將刪除成功的訊息放入 connection,並重導到 index 頁面。
注意在這裡如果刪除失敗時,會導致 MatchError
,所以如果你的應用程式變得較複雜,有可能發生刪除失敗的情況,需要改寫這段以防止應用程式錯誤。
歸納來看,在 Controller 的各個 action 裡,會依序發生這些事:
Rails 框架的 controller 有提供 before_action、after_action 及 around_action,用於宣告此 Controller 每個 action 都需要進行的前置及後置處理。
仔細想一下,我們之前在 Router 裡好像有個很類似的概念,叫做 pipeline。而既然 Controller 也是個 plug,所以他也有自己的 pipeline。但與 Router 的 pipeline 不同的有兩點: 1. 一個 controller 只有一條 pipeline。2. 可以使用 guard
用起來大概像是這樣:
defmodule UserController do
use MyAppWeb, :controller
plug :authenticate, [usernames: ["j", "e"]] when action in [:show, :edit]
plug :authenticate, [usernames: ["admin"]] when not action in [:index]
# actions
defp authenticate(conn, options) do
if get_session(conn, :username) in options[:usernames] do
conn
else
conn |> redirect(to: "/") |> halt()
end
end
end
在 controller 的 pipeline,可以用於 guard 判斷的變數有 conn
、action
及 controller
(會拿到目前的 controller 名稱)。
但由於每個 action 都要主動控制流程,而不是像 Rails controller action 的方法本體可以只指派模組變數,Phoenix 的 Controller 沒有提供 after_action 及 around_action,而是在流程中適當的地方 pipe 過自行宣告的函式。
在後續的章節裡,將解釋 Controller 跟 Context、Schema 及 View 互動的細節,及 1.3 版新增的 Action Fallback 機制。除此之外,controller 還會做一些其它的事情:
在剛才新增失敗的例子裡,就有看到這樣的用例:
conn
|> redirect(to: post_path(conn, :index))
redirect
接收的 keyword list 參數中,to:
的值會是重導向目標路徑的字串。而 post_path(conn, :index)
會回傳該字串。這部份的細節,請參考上一篇的 Path Helper 部份。
也可以用 external:
來重新導向到外部連結:
def index(conn, _params) do
redirect conn, external: "https://elixir-lang.org/"
end
當流程中有經過 halt/1
時,結束這個呼叫後,會中止所有後續的流程 (通常是 render),直接回傳 connection。大多用於處理錯誤發生的情況。
conn
|> send_resp(404, "Not found")
|> halt()
除此之外,controller 還可以設定 content-type,HTTP status 及用 send_resp
直接回傳 HTTP 回應等等。詳細的說明也請參考 Phoenix.Controller 文件吧。
下一篇要來解說 Controller 所呼叫的 Context 了。
Happy hacking! 明天見。