Phoenix のコントローラで render/3 を呼んでからのレンダリング処理の流れを追う

Phoenix のコントローラでは `render(conn, "index.html", foos: foos)` のように `render/3` を呼ぶことでビューをレンダリングすることができる。もろもろの事情があって、この処理の途中にフックして引数の値を書き換えてみたいと思った。

というわけで、コントローラで `render/3` を呼んだら何がどうなって HTML がレンダリングされるのか追いかけてみた。

## ネタバレ：この記事を読んで得られること

この記事では、生成されたコードで使われているもののどこで定義されているか分からない関数から始まって、マクロで生成された関数定義に辿り着くまでを順を追って書いていきます。マクロを駆使した Elixir のコードにおいて、関数がどこで定義されているかを探す方法や考え方のヒントを得られる……はずです。分かりづらい箇所があればお気軽にコメントをください。

## 準備：プロジェクトを作る

（ここは読み飛ばしてもOK）

説明用に新規アプリケーション `my_app` を作る。 `mix phoenix.gen.html` コマンドで `Foo` モデル（と関連するビューとコントローラ他）を生成する。

```bash
mix phoenix.new my_app --no-brunch
cd my_app
mix do deps.get, compile
mix phoenix.gen.html Foo foos bar:string
```

## 出発：FooController

出発点は `MyApp` アプリケーションの `FooController` 。 `index/2` の中で `render/3` を呼んでいる。

```ex:web/controllers/foo_controller.ex
defmodule MyApp.FooController do
  use MyApp.Web, :controller

  alias MyApp.Foo

  plug :scrub_params, "foo" when action in [:create, :update]

  def index(conn, _params) do
    foos = Repo.all(Foo)
    render(conn, "index.html", foos: foos)  # ← ここから出発
  end

  # ...
end
```

この `render/3` は何者だろう？　どこで定義されているのだろうか？

## 関数はどこから来るか

Elixir のモジュール内で関数を呼んでいるとき、その関数がどこで定義されているかは4つの可能性に絞られる：

- そのモジュール内の `def` または `defp` で定義している
- そのモジュール内の `import OtherModule` で他のモジュールからインポートしている
- そのモジュール内の `use OtherModule` が、 `def` か `defp` か `import` か `use` か `@before_compile` に展開される
- そのモジュール内の `@before_compile` 属性で指定したマクロが、 `def` か `defp` か `import` か `use` に展開される

`def` または `defp` で定義しているのでなければ、 `import` または `use` しているモジュールのソースを見てみる。その中でもまた `import` か `use` しているかもしれないが、先を追っていくといずれ `def` または `defp` に辿りつくはずだ。

### 補足

（ここは読み飛ばしてもOK）

関数の定義の場所について、厳密には他の可能性もある。ひとつは Elixir の組み込み関数として `Kernel` モジュールに定義されている可能性だが、これは今回は考えなくていい。

また、動的に関数を定義する手段として eval 系の関数がある：

```ex
defmodule MyModule do
  Code.eval_string("def answer, do: 42", [], __ENV__)
end
# MyModule.answer => 42
```

とはいえ、 Elixir はマクロが十分にパワフルなので eval を目にする機会はほとんどないだろう。今回のプロジェクトで `grep eval` してみると、 eval が使われているのは ecto/lib/ecto/schema.ex で `Module.eval_quoted/4` を呼んでいる1箇所だけだった。

## `use MyApp.Web, :controller`

さて、 `MyApp.FooController` に `def render` は見当たらない。とすれば `render/3` はモジュールの外から来ていることになる。

怪しいのは `use MyApp.Web, :controller` だ。

```ex:web/controllers/foo_controller.ex
defmodule MyApp.FooController do
  use MyApp.Web, :controller  # ←これ
  # ...
end
```

`MyApp.Web` は、各コントローラやビューに共通する定義をまとめたモジュールだ。各コントローラの先頭に `use MyApp.Web, :controller` と1行書けば複数の `use` や `import` に展開される。

`MyApp.Web` の中身を見てみよう。

```ex:web/web.ex
defmodule MyApp.Web do
  # ...

  def controller do
    quote do
      use Phoenix.Controller

      alias MyApp.Repo
      import Ecto.Model
      import Ecto.Query, only: [from: 2]

      import MyApp.Router.Helpers
    end
  end
  
  # ...

  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end
```

`__using__/1` マクロが定義されている。別のモジュールで `use MyApp.Web, :controller` すると `MyApp.Web.controller/0` を呼ぶ仕掛けだ。その返り値の quote されたコード片は、結局 `use` した側のモジュールに埋め込まれる。

`controller/0` が返すコード片にはいくつかの `use` と `import` があって、他のモジュールから関数を取り込んでいる。

ようするに、ここの `use` と `import` が取り込んだ関数は `MyApp.FooController` 内に埋め込まれるということだ。この中のどれかが `render/3` を取り込んでいるのだろうか？

この記事ではそれぞれのモジュールを調べる過程は省略しよう。マニュアルやコードを調べた結果、探している関数 `render/3` は `use Phoenix.Controller` から来ていることが分かった。

## `use Phoenix.Controller`

ここからは Phoenix のコードを追っていく。 Phoenix の関数を調べるときは API ドキュメント http://hexdocs.pm/phoenix/ を読むといいだろう。ドキュメント中の “Source” をクリックすると対応するコードを GitHub で開くこともできる。

ドキュメントのサイドバーで `render` を検索すると何件か候補が出てくる。 `Phoenix.Controller.render` のどれかが正解だ。

## `Phoenix.Controller.render`

 `Phoenix.Controller.render` には arity が2, 3, 4とあるが、ソースを読むとそのどれもプライベート関数 `do_render/4` を呼んでいる。 `do_render/4` はこうなっている：

```ex:phoenix/controller.ex
  defp do_render(conn, template, format, assigns) do
    assigns = to_map(assigns)
    content_type = Plug.MIME.type(format)
    conn =
      conn
      |> put_private(:phoenix_template, template)
      |> prepare_assigns(assigns, format)

    view = Map.get(conn.private, :phoenix_view) ||
            raise "a view module was not specified, set one with put_view/2"
    data = Phoenix.View.render_to_iodata(view, template,
                                         Map.put(conn.assigns, :conn, conn))
    send_resp(conn, conn.status || 200, content_type, data)
  end
```

( https://github.com/phoenixframework/phoenix/blob/v0.15.0/lib/phoenix/controller.ex#L571-L584 )

引数について補足しておくと、 `conn` は `Plug.Conn` 構造体だ。 `template` はテンプレートの名前。ひとつ飛ばして`assigns` は `Dict` な値。これらは一番最初の `render/3` に渡したものと同じ。ここでは `template = "index.html"`, `assigns = [foos: foos]` になっている。

残る `format` 引数はテンプレートのフォーマット。テンプレート名 `"index.html"` に対応するテンプレートファイルは web/templates/page/index.html.eex だが、この最後の拡張子がフォーマットになる。つまり `format = "eex"` だ。

また、関数内で `view = Map.get(conn.private, :phoenix_view)` としているが、ここで `view` は `MyApp.FooView` になる。

この関数はどうやら `Phoenix.View.render_to_iodata/3` を呼んでテンプレートをレンダリングしているようだ。

## `Phoenix.View.render_to_iodata/3`

```ex:phoenix/view.ex
  def render_to_iodata(module, template, assign) do
    render(module, template, assign) |> encode(template)
  end
```

( https://github.com/phoenixframework/phoenix/blob/v0.15.0/lib/phoenix/view.ex#L431-L433 )

`render_to_iodata/3` は `render/3` を呼んでいる。 それを `encode/2` に渡して iodata なるものに変換しているようだが、そっちは措いておこう。 `render/3` を見てみる。

## `Phoenix.View.render/3`

```ex:phoenix/view.ex
  def render(module, template, assigns) do
    assigns
    |> to_map()
    |> Map.pop(:layout, false)
    |> render_within(module, template)
  end
```

( https://github.com/phoenixframework/phoenix/blob/v0.15.0/lib/phoenix/view.ex#L188-L193 )

若干ややこしいことになっているのでコメントをつけてみた。

```ex
  def render(module, template, assigns) do
    assigns     # == %{conn: conn, foos: foos}
                # 元々は [foos: foos] だったが、
                # `do_render/4` によって `conn` が追加され、
                # さらに `render_to_iodata/3` の中で
                # `to_map(assigns)` によって Map になっている。

    |> to_map() # …のでこれは noop

    |> Map.pop(:layout, false) # assigns から :layout キーの値を取り出す。
                               # ここでは存在しないので {false, assigns} が返る
                               
    |> render_within(module, template)
  end
```

最後には `render_within({false, assigns}, module, template)` を呼んでいる。これは `Phoenix.View` のプライベート関数だ。

## `Phoenix.View.render_within/3`

```ex:phoenix/view.ex
  defp render_within({{layout_mod, layout_tpl}, assigns}, inner_mod, template) do
    # ...
  end

  defp render_within({false, assigns}, module, template) do
    template
    |> module.render(assigns)
  end
```

( https://github.com/phoenixframework/phoenix/blob/v0.15.0/lib/phoenix/view.ex#L195-L209 )

候補が2つあるが、第1引数に `{false, assigns}` を渡してマッチするのは2つ目のほうだ。 `template |> module.render(assigns)` とあるが、 `module` は何だろう？

答えはこの記事を `Phoenix.Controller.render` まで遡ったところにある。 `view = MyApp.FooView` とあり、それがここへきて `module` にセットされている。

つまりここでは `MyApp.FooView.render(template, assigns)` を呼んでいるということだ。

## `MyApp.FooView.render/2`

また `MyApp` のコードに戻ってきた。 `FooView` はどうなっているかというと……

```ex:web/views/foo_view.ex
defmodule MyApp.FooView do
  use MyApp.Web, :view
end
```

これだけ。 `render/2` は `use MyApp.Web, :view` から来ているに違いない。

`MyApp.Web` はいくつかの `use` や `import` を束ねるモジュールだったのを覚えているだろうか。中身を読む過程は一度やったので省略しよう。 `render/2` を提供しているのは、そのうちの `use Phoenix.View, root: "web/templates"` だ。

## `use Phoenix.View`

`use Phoenix.View` すると `Phoenix.View.__using__/1` マクロが呼ばれる。

```ex:phoenix/view.ex
  defmacro __using__(options) do
    if root = Keyword.get(options, :root) do
      namespace =
        # ...

      quote do
        import Phoenix.View

        use Phoenix.Template, root:
          Path.join(unquote(root),
                    Phoenix.Template.module_to_template_root(__MODULE__, unquote(namespace), "View"))

        # ...
      end
    else
      raise "expected :root to be given as an option"
    end
  end
```

( https://github.com/phoenixframework/phoenix/blob/v0.15.0/lib/phoenix/view.ex#L120-L147 )

ここには `render/2` の定義はない。とすれば `use Phoenix.Template, root: ...` の中だ。

## `use Phoenix.Template`

```ex:phoenix/view.ex
  defmacro __using__(options) do
    root = Dict.fetch! options, :root

    quote do
      @template_root Path.relative_to_cwd(unquote(root))
      @before_compile unquote(__MODULE__)

      @doc """
      Renders the given template locally.
      """
      def render(template, assigns \\ %{})

      def render(template, assigns) when is_list(assigns) do
        render(template, Enum.into(assigns, %{}))
      end

      def render(module, template) when is_atom(module) do
        Phoenix.View.render(module, template, %{})
      end

      # ...
    end
  end
```

( https://github.com/phoenixframework/phoenix/blob/v0.15.0/lib/phoenix/template.ex#L110-L147 )

ついに `render/2` の定義が出てきた！

だが、よく見ると1つ目には本体がなく、2つ目にはガードがついている。3つ目にもガードがあるうえに引数が `(module, template)` と他とは違っている。

この時点で、 `render/2` の引数は `(template, assigns)` になっているはずだ（思い出せなければこの記事を `MyApp.FooView.render(template, assigns)` で検索）。さらに `assigns` は Map になっているので、これら3つの `render/2` のどれにもマッチしない。

とすれば、残るは `@before_compile unquote(__MODULE__)` が怪しい。

この属性がここにあると、 `use Phoenix.Template` した側のモジュールに `Phoenix.Template.__before_compile__/1` マクロが返すコード片が埋め込まれることになる（理由は Elixir の Module のドキュメントを参照 http://elixir-lang.org/docs/v1.0/elixir/Module.html ）。

そのコード片の中に求める `render/2` があるはずだ。

## `Phoenix.Template.__before_compile__/1`

以下が `__before_compile__/1` のコード。少し長いが重要な箇所なので省略せずに掲載する。後半の `quote line: -1 do ... end` が返り値となるコード片だ。

```ex:phoenix/template.ex
  defmacro __before_compile__(env) do
    root = Module.get_attribute(env.module, :template_root)

    pairs = for path <- find_all(root) do
      compile(path, root)
    end

    names = Enum.map(pairs, &elem(&1, 0))
    codes = Enum.map(pairs, &elem(&1, 1))

    # We are using line -1 because we don't want warnings coming from
    # render/2 to be reported in case the user has defined a catch all
    # render/2 clause.
    quote line: -1 do
      unquote(codes)

      def render(tpl, %{render_existing: {__MODULE__, tpl}}) do
        nil
      end
      def render(template, assigns) do
        template_not_found(template, assigns)
      end

      @doc """
      Returns the template root alongside all templates.
      """
      def __templates__ do
        {@template_root, unquote(names)}
      end

      @doc """
      Returns true whenever the list of templates changes in the filesystem.
      """
      def __phoenix_recompile__? do
        unquote(hash(root)) != Template.hash(@template_root)
      end
    end
  end
```

( https://github.com/phoenixframework/phoenix/blob/v0.15.0/lib/phoenix/template.ex#L150-L187 )

この中で `render/2` の定義は2箇所。繰り返すが、ここで `render/2` に与える引数は `(template, assigns)` だ。そしてこのとき `assigns = %{conn: conn, foos: foos}` となっている。

とすると、1つ目の `def render(tpl, %{render_existing: {__MODULE__, tpl}})` は第2引数のパターンにマッチしない。

残る2つ目には無事マッチするが、2つ目は `template_not_found(template, assigns)` という不穏な関数を呼んでいる。そしてこの関数は見た目どおり例外を発生させる！

ここまで来て行き詰まってしまった。冒頭で挙げた関数定義の4つのパターンのどれにも当てはまらない。 `def render` はあるがレンダリング処理ではなく例外を投げている。 `use` も `import` も見あたらないし `@before_compile` もない。

気を取り直してもう一度コードを見てみよう。この中で一見してそれと分からない `render/2` の定義が現れる余地があるとすれば、それは `quote line: -1 do` 直後の `unquote(codes)` だ。仮にここで `render/2` が定義されていれば、その下に続く `def render` よりも先にマッチすることになる。

`codes` は何かというと、 `compile(path, root)` を何回も呼び、その返り値を集めて作られている。 `compile/2` は同じモジュール内で定義されたプライベート関数だ。見てみよう。

## `Phoenix.Template.compile/2`

```ex:phoenix/template.ex
  defp compile(path, root) do
    name   = template_path_to_name(path, root)
    defp   = String.to_atom(name)
    ext    = Path.extname(path) |> String.lstrip(?.) |> String.to_atom
    engine = Map.fetch!(engines(), ext)
    quoted = engine.compile(path, name)

    {name, quote do
      @file unquote(path)
      @external_resource unquote(path)

      defp unquote(defp)(var!(assigns)) do
        _ = var!(assigns)
        unquote(quoted)
      end

      def render(unquote(name), assigns) do
        unquote(defp)(assigns)
      end
    end}
  end
```

( https://github.com/phoenixframework/phoenix/blob/v0.15.0/lib/phoenix/template.ex#L315-L335 )

`def render(unquote(name), assigns)` がある！　この `name` の値を求めるにも紆余曲折あるのだが、すっ飛ばして答えを書いてしまうと `MyApp.FooView` に関連づけられたテンプレートの名前のどれかになる。 `"index.html"` や `"edit.html"` などだ。

`compile/2` を何回も呼んでいるとすぐ上で書いたが、正確に言えば `MyApp.FooView` に関連するテンプレートファイルのパスすべてを順に引数にして `compile/2` を呼んでいるのだ。

とすると、この `def render` は次のように展開される：

```ex
def render("index.html", assigns) do
  quote(defp)(assigns)
end

# 注意： quote(defp) は :"index.html" に展開されるが、
# 実際のコードで文字通り :"index.html"(assigns) と書くと構文エラーになる。
# quote do ... end の中でだけこういう書き方が許される。
# 実際のコードで記号を含む名前の関数を呼び出すには MyModule."foo.bar"() のようにする。
```

無事に `render/2` の定義が現れた。この先を辿るとテンプレートエンジンまで行き着くが、今回はここまでにしておこう。

この `render/2` の定義は `@before_compile` のはたらきで `MyApp.View` に埋め込まれることになる。

## まとめ：何がどうなっていたのか

Phoenix の `MyApp.FooController` で `render(conn, "index.html", foos: foos)` を呼ぶと、 `MyApp.FooView.render/2` の呼び出しに行き着く。この `render/2` はマクロによって次のように定義されている：

```ex:web/views/foo_view.ex
defmodule MyApp.FooView do
  use MyApp.Web, :view
  
  # ---- ↓マクロによって挿入される
  
  def render("index.html", assigns) do
    # テンプレートエンジンを呼ぶ
  end
  def render("edit.html", assigns) do
    # テンプレートエンジンを呼ぶ
  end
  # ...
  
  # ----
end
```

もしも `MyApp.FooView.render/2` の処理にフックしたいなら、 `MayApp.FooView` に `render/2` を定義すればいい。ただし最終的にはマクロによって挿入された `render/2` を呼ばなければならないのでパターンマッチを工夫すること。さもないと自分で定義したほうの関数が再帰的に呼ばれることになる。

```ex:web/views/foo_view.ex
defmodule MyApp.FooView do
  use MyApp.Web, :view
  
  def render("index.html", %{foos: foos} = assigns) do
    assigns = assigns
    |> Map.drop([:foos])          # 以降この render/2 にマッチさせない
    |> Map.put(:new_foos, ...)
    render("index.html", assigns) # マクロによって挿入されたほうの render/2 を呼ぶ
  end
  
  # ---- ↓マクロによって挿入される
  # 略
  # ----
end
```