Phoenixガイドラインを読んでみた(5) - Controllers -

## はじめに

[前回](http://qiita.com/dynamitecoolguy/items/01df54b9eea28a0b1eb9)は、Plugを見ていきました。
今回は[Controller](http://www.phoenixframework.org/docs/controllers)です。
[次回](http://qiita.com/dynamitecoolguy/items/894c394faf9c3690ec5d)はViewです。

## 概要

Controllerには、Action (RouterによってHTTPリクエストに対するレスポンスを行うための関数) を定義しています。
Actionは、Templateをレンダリングしたり、JSONレスポンスを返したりします。

PhoenixのControllerはPlugパッケージをベースに作成されていて、Controller自身もPlugです。
Phoenixで新規にアプリケーションを作成したとき、ルート用のControllerとして`PageController`が定義されます(`web/controllers/page_controller.ex`)。

```
defmodule HelloPhoenix.PageController do
  use HelloPhoenix.Web, :controller

  def index(conn, _params) do
    render conn, "index.html"
  end
end
```

`use HelloPhoenix.Web, :controller`は、HelloPhoenix.Web(`web/web.ex`)のcontroller関数を呼び出します。
また、HelloPhoenix.Router(`web/router.ex`)に、デフォルトルート用のController定義が生成されます。

```
  scope "/", HelloPhoenix do
    ...
    get "/", PageController, :index
```

## Action

Actionは関数であり、Elixirの命名規則に従っていればどんな関数名でもかまいません。
デフォルトの`:index`を変更したり、"?"を付けることもできます。

ただし、indexやrouteのresourcesで作成されるAction(show, new, createなど)は変更しない方が分かりやすいです。

Actionは、常にパラメータを二つとる関数で、第一引数は、Plugミドルウェアで生成された接続情報を表す[パラメータ](http://hexdocs.pm/plug/Plug.Conn.html)です。第二引数は、リクエストから引き回されてきたパラメータのmapです。

## データアクセス

Phoenix自身はデータアクセスレイヤーを持たずに、EctoによるDBアクセスを提供しています。
もちろん、OTPのETSやDETS、mnesia、他のデータアクセスライブラリを使用することもできます。

## フラッシュメッセージ

ユーザアクションに応じて簡単なメッセージを返すようなケースがあります。たとえば、エラーによる更新失敗であったり、アプリケーションへのウェルカムメッセージなどです。こういうケースでは、フラッシュメッセージが有効です。

Phoenix.Controllerでは、`put_flash/3`と`get_flash/2`というフラッシュメッセージのKey-Valueペア用の関数が提供されています。
以下に、二つのフラッシュメッセージをセットする例を示します。
また、フラッシュメッセージを消去する`clear_flash/1`という関数もあります。

```
defmodule HelloPhoenix.PageController do
  . . .
  def index(conn, _params) do
    conn
    |> put_flash(:info, "Welcome to Phoenix, from flash info!")
    |> put_flash(:error, "Let's pretend we have an error.")
    |> render("index.html")
  end
end
```

フラッシュメッセージのキーに特に決まりはありませんが、`:info`や`:error`というのは一般的です。

フラッシュメッセージを表示するためには、Template側でフラッシュメッセージを`get_flash/2`で取得して表示する必要があります。
なお、デフォルトのレイアウト(`web/templates/layout/app.html.eex`)にはその仕組みが備わっています。

```
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
```

## レンダリング

Controllerにおいて、コンテンツのレンダリングするには何種類か方法があります。もっとも簡単なのはPhoenixが提供する`text/2`関数です。

```
def show(conn, %{"id" => id}) do
  text conn, "Showing id #{id}"
end
```

`get "/path/:id"`をこの`show`関数にマッピングされているとします。ブラウザから`/path/15`をリクエストした場合、想像通り、plain-textで"Showing id 15"が返されます。

同様に`json/2`関数を使えば、JSONを返すことができます。なお、Map型などをJSONに変換するには[Poisonライブラリ](https://github.com/devinus/poison) (Phoenixの依存ライブラリ)を使用しています。
`show`を以下のように変更すると、`/path/15`をリクエストに対して`{ id: "15" }`を返すようになります。

```
def show(conn, %{"id" => id}) do
  json conn, %{id: id}
end
```

Phoenixでは、テンプレートを使わずにHTMLを返すこともできます。`text`、`json`と同様に`html/2`関数を使用します。使用方法は想像通りです。
ただ、テンプレートと違って`<%= id %>`といった書き方はできません。

```
def show(conn, %{"id" => id}) do
  html conn, """
     <html>
       <head>
          <title>Passing an Id</title>
       </head>
       <body>
         <p>You sent in id #{id}</p>
       </body>
     </html>
    """
end
```

直接値を出力する関数以外にも、テンプレートを使って表示する方法も一般的です。テンプレートを使って表示する場合は、`render/3`関数を使用します。

なお、内部的には、`render/3`関数はPhoenix.Viewモジュールで定義されていますが、利便性の理由からPhoenix.Controllerにも定義しています。

テンプレートを使う例は、以下の通りです。

```
defmodule HelloPhoenix.HelloController do
  use HelloPhoenix.Web, :controller

  def show(conn, %{"messenger" => messenger}) do
    render conn, "show.html", messenger: messenger
  end
end
```

`render/3`関数を使用するために、それぞれの名前の先頭を統一する必要があります。
例えば、`HelloController`からは`HelloView`を参照しますし、`HelloView`は`web/templates/hello`ディレクトリを参照し、そのディレクトリ内には`render/3`で指定したテンプレート(上記の例だと"show.html.eex")が無ければなりません。

`render/3`には、第三引数としてパラメータを渡しますが、関数内でこれを制御することもできます。

```
def index(conn, _params) do
  conn
  |> assign(:message, "Welcome Back!")
  |> render("index.html")
end
```

上記の例をplugを使って書くこともできます。

```
plug :assign_welcome_message, "Welcome Back"

def index(conn, _params) do
  conn
  |> render("index.html")
end

defp assign_welcome_message(conn, msg) do
  assign(conn, :message, :msg)
end
```

なお、plugには指定されたアクションにだけ適用されるような書き方もあります。

```
  plug :assign_welcome_message, "Hi!" when action in[:index]
```

### レスポンス

200以外のレスポンスを返す場合には、`send_resp/3`を使います。また、レスポンスのcontent-typeを指定する場合には、`put_resp_content_type/2`を使用します。

```
def index(conn, _params) do
  conn
  |> put_resp_content_type("text/plain")
  |> send_resp(201, "")
end
```

### レイアウト

Layoutは、テンプレートのサブセットで、`/web/templates/layout`に配置されます。Phoenixでは、デフォルトで`app.html.eex`というレイアウトを生成し、全てのテンプレートがこのレイアウト上に表示されます。
LayoutはあくまでTemplateに過ぎませんので、表示する際にはViewが必要になります。`LayoutView`モジュールが`/web/views/layout_view.ex`に定義されます。Layoutを`/web/templates/layout`に配置する限りは、Layout用の新しいViewを生成する必要はありません。

`Phoenix.Controller`モジュールの`put_layout/2`関数を使って、使用するLayoutを変更することができます。この関数に`false`を指定することでレイアウト無しで表示させることもできます。

```
def index(conn, params) do
  conn
  |> put_layout(false)                # ← ()を忘れないように
  |> render "index.html"
end
```

### レンダリングフォーマット

同じ関数でHTMLやplain-textなどのフォーマットを切り替えたい場合は、以下のようにします。

```
defmodule HelloPhoenix.Router do
  use HelloPhoenix.Web, :router

  pipeline :browser do
    plug :accepts, ["html", "text"]
    ...
  end

  def index(conn, _params) do
    render conn, :index
  end
  ...  
```

`http://localhost:4000/?_format=text`とすると、"index.text.eex"を参照し、htmlだと"index.html.eex"を参照するようになります。

### Content-Typeのセット

xmlを出力したい場合は、以下のようにします。

```
def index(conn, _params) do
  conn
  |> put_resp_content_type("text/xml")
  |> render "index.xml", content: some_xml_content
end
```

### HTTPステータス

`put_status/2`を使って、200以外のステータスに変更することができます。

```
def index(conn, _params) do
  conn
  |> put_status(202)
  |> render("index.html")
end
```

### リダイレクト

リダイレクトするには、`redirect/2`関数を使います。

```
defmodule HelloPhoenix.Router do
  use HelloPhoenix.Web, :router
  . . .

  scope "/", HelloPhoenix do
    . . .
    get "/", PageController, :index
  end

  # New route for redirects
  scope "/", HelloPhoenix do
    get "/redirect_test", PageController, :redirect_test, as: :redirect_test
  end
end

defmodule HelloPhoenix.PageController do
  . . .
  def index(conn, _params) do
    redirect conn, to: "/redirect_test"
  end

  def redirect_test(conn, _params) do
    text conn, "Redirect!"
  end
  . . .
end
```

外部へのリダイレクトの場合、`to:`ではなく`external:`を使います


```