Phoenix 1.3のディレクトリ構造とContext


Phoenix 1.3のrc-2が先月リリースされそろそろPhoenix 1.3にアプデも考えだしてきた今日この頃、まだアップデートはしないけど1.3正式版がリリースされることに備えてディレクトリ構造とか学んでおきたい
ということでざっと慣れるためにPhoenix 1.3を触ってみます

## What's changed in Phoenix 1.3

Phoenix 1.3での主な変更点は以下です

- `web`ディレクトリが`lib/<project>/web`以下になった
- デフォルトのジェネレータにContextという概念が追加されアプリケーション境界を分割出来るようになった
- Fallback Action Plugが追加された
  - Controllerの構成がシンプルに出来るようになった

## ディレクトリ構造

まず大きく変わった点としてデフォルトのディレクトリ構造が変わったということもあるので、phoenixプロジェクトを一から作成してみます

プロジェクト作成のコマンドの名前が`phoenix.new`から`phx.new`に変更になり1.2と1.3で意識的に変えたことが分かりますね

```bash
$ mix phx.new my_app  
* creating my_app/config/config.exs
* creating my_app/config/dev.exs
* creating my_app/config/prod.exs
* creating my_app/config/prod.secret.exs
* creating my_app/config/test.exs
* creating my_app/lib/my_app/application.ex
* creating my_app/lib/my_app/web/channels/user_socket.ex
* creating my_app/lib/my_app/web/views/error_helpers.ex
* creating my_app/lib/my_app/web/views/error_view.ex
* creating my_app/lib/my_app/web/endpoint.ex
* creating my_app/lib/my_app/web/router.ex
* creating my_app/lib/my_app/web/web.ex
* creating my_app/mix.exs
* creating my_app/README.md
* creating my_app/test/support/channel_case.ex
* creating my_app/test/support/conn_case.ex
* creating my_app/test/test_helper.exs
* creating my_app/test/my_app/web/views/error_view_test.exs
* creating my_app/lib/my_app/web/gettext.ex
* creating my_app/priv/gettext/en/LC_MESSAGES/errors.po
* creating my_app/priv/gettext/errors.pot
* creating my_app/lib/my_app/repo.ex
* creating my_app/priv/repo/seeds.exs
* creating my_app/test/support/data_case.ex
* creating my_app/lib/my_app/web/controllers/page_controller.ex
* creating my_app/lib/my_app/web/templates/layout/app.html.eex
* creating my_app/lib/my_app/web/templates/page/index.html.eex
* creating my_app/lib/my_app/web/views/layout_view.ex
* creating my_app/lib/my_app/web/views/page_view.ex
* creating my_app/test/my_app/web/controllers/page_controller_test.exs
* creating my_app/test/my_app/web/views/layout_view_test.exs
* creating my_app/test/my_app/web/views/page_view_test.exs
* creating my_app/.gitignore
* creating my_app/assets/brunch-config.js
* creating my_app/assets/css/app.css
* creating my_app/assets/css/phoenix.css
* creating my_app/assets/js/app.js
* creating my_app/assets/js/socket.js
* creating my_app/assets/package.json
* creating my_app/assets/static/robots.txt
* creating my_app/assets/static/images/phoenix.png
* creating my_app/assets/static/favicon.ico

Fetch and install dependencies? [Yn] n

We are almost there! The following steps are missing:

    $ cd my_app
    $ mix deps.get
    $ cd assets && npm install && node node_modules/brunch/bin/brunch build

Then configure your database in config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server
```

構造を確認してみます

```bash
$ tree
.
├── assets
│   ├── brunch-config.js
│   ├── css
│   │   ├── app.css
│   │   └── phoenix.css
│   ├── js
│   │   ├── app.js
│   │   └── socket.js
│   ├── package.json
│   ├── static
│   │   ├── favicon.ico
│   │   ├── images
│   │   │   └── phoenix.png
│   │   └── robots.txt
│   └── vendor
├── config
│   ├── config.exs
│   ├── dev.exs
│   ├── prod.exs
│   ├── prod.secret.exs
│   └── test.exs
├── lib
│   └── my_app
│       ├── application.ex
│       ├── repo.ex
│       └── web
│           ├── channels
│           │   └── user_socket.ex
│           ├── controllers
│           │   └── page_controller.ex
│           ├── endpoint.ex
│           ├── gettext.ex
│           ├── router.ex
│           ├── templates
│           │   ├── layout
│           │   │   └── app.html.eex
│           │   └── page
│           │       └── index.html.eex
│           ├── views
│           │   ├── error_helpers.ex
│           │   ├── error_view.ex
│           │   ├── layout_view.ex
│           │   └── page_view.ex
│           └── web.ex
├── mix.exs
├── priv
│   ├── gettext
│   │   ├── en
│   │   │   └── LC_MESSAGES
│   │   │       └── errors.po
│   │   └── errors.pot
│   └── repo
│       ├── migrations
│       └── seeds.exs
├── README.md
└── test
    ├── my_app
    │   └── web
    │       ├── channels
    │       ├── controllers
    │       │   └── page_controller_test.exs
    │       └── views
    │           ├── error_view_test.exs
    │           ├── layout_view_test.exs
    │           └── page_view_test.exs
    ├── support
    │   ├── channel_case.ex
    │   ├── conn_case.ex
    │   └── data_case.ex
    └── test_helper.exs

29 directories, 41 files
```

まず大きな点として1.2でルートにあった`web`が`lib`以下にまとめられたことで通常のElixirライブラリと同じような構成になっています。

1.2ではRailsライクなフレームワークという側面が強かったですが1.3になってElixirライブラリ標準を意識してきた感がありますね。ちなみにPhoenix 1.2でなぜ`web`というディレクトリなのかというと、技術的な制限からそうなったそうです。

## Context

Phoenix 1.3からはデフォルトのジェネレータに`mix phx.gen.context`が追加され、Contextという概念が持ち込まれました。
DDDとかやられている方にはお馴染みの概念かもしれませんが簡単に説明するとDDDではある一定のドメイン知識のまとまりを表すのにコンテキストという言葉が使われています。

Elixir ForumでもDDDのContextの概念を持ち込むのは新規参入者の壁になるのでは？という[議論もされていた](https://elixirforum.com/t/contexts-a-barrier-too-high-for-newbies/5929)Contextですが実際使ってみた感じ必ずしもDDDと同じように考える必要はなく、どちらかというとカジュアルにアプリケーションの境界を考える手段として提供されているものだと思いました。

個人的にもDDDに関しては「りろんはしってる」レベルなので最初はガチガチなDDD来たらどうしようと身構えていたけど実際使ってみるとなるほどという感想になりました。
ここは実際にphoenixジェネレータのContext関係のコマンドで出力されるファイルを見た方が理解が早いと思うので見てみましょう

```bash
$ mix phx.gen.context Accounts User users name:string

* creating ./lib/my_app/accounts/user.ex
* creating priv/repo/migrations/20170625042439_create_accounts_user.exs
* creating ./lib/my_app/accounts/accounts.ex
* injecting ./lib/my_app/accounts/accounts.ex
* creating ./test/my_app/accounts/accounts_test.exs
* injecting ./test/my_app/accounts/accounts_test.exs

Remember to update your repository by running migrations:

    $ mix ecto.migrate
```

`phx.gen.context`に第一引数でコンテキスト名、第二第三でコンテキスト内に生成するモデルのsingularとpluralな名前、最後に任意の数のフィールド名とフィールド型のペアを渡しています。
結果、生成されたファイルを見ると第一引数のコンテキスト名(`accounts`)でPhoenixプロジェクト内にディレクトリが作られ、その中にスキーマ（`user.ex`）及びコンテキスト（`accounts.ex`）が作られていることが分かります。

スキーマは以下のような感じ。必要最低限ですね。

```elixir
$ cat user.ex 
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias DevApp.Accounts.User


  schema "accounts_users" do
    field :name, :string

    timestamps()
  end

  @doc false
  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end
```

`accounts.ex`としてコンテキスト名のファイルが作られています

```elixir
$ cat accounts.ex 
defmodule MyApp.Accounts do
  @moduledoc """
  The boundary for the Accounts system.
  """

  import Ecto.Query, warn: false
  alias MyApp.Repo

  alias MyApp.Accounts.User

  @doc """
  Returns the list of users.

  ## Examples

      iex> list_users()
      [%User{}, ...]

  """
  def list_users do
    Repo.all(User)
  end

  @doc """
  Gets a single user.

  Raises `Ecto.NoResultsError` if the User does not exist.

  ## Examples

      iex> get_user!(123)
      %User{}

      iex> get_user!(456)
      ** (Ecto.NoResultsError)

  """
  def get_user!(id), do: Repo.get!(User, id)

  @doc """
  Creates a user.

  ## Examples

      iex> create_user(%{field: value})
      {:ok, %User{}}

      iex> create_user(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a user.

  ## Examples

      iex> update_user(user, %{field: new_value})
      {:ok, %User{}}

      iex> update_user(user, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a User.

  ## Examples

      iex> delete_user(user)
      {:ok, %User{}}

      iex> delete_user(user)
      {:error, %Ecto.Changeset{}}

  """
  def delete_user(%User{} = user) do
    Repo.delete(user)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking user changes.

  ## Examples

      iex> change_user(user)
      %Ecto.Changeset{source: %User{}}

  """
  def change_user(%User{} = user) do
    User.changeset(user, %{})
  end
end
```

`The boundary for the Accounts system.`というmoduledocから見る通りアカウントシステムというバウンダリを提供するモジュールという位置づけなのが分かります。
各コンテキスト内にスキーマを定義出来るので例えばアカウントシステムとバックエンドシステムでモデルを切り変えたい場合もコンテキストを切り替えるだけでいいことが分かります。

実際の例としては[hex.pm](https://hex.pm/)の[リポジトリ](https://github.com/hexpm/hexpm)が既にPhoenix 1.3に切り替わっていて運用されているので例として参考になると思います。

### Difference between DDD's Context

ちなみにPhoenix作者のChris McCordは説明のためにDDDを引用しましたが**DDDのContextとは違う**ものだと[Joséは説明している](https://elixirforum.com/t/how-to-determine-contexts-with-phoenix-1-3/4367/37)ので、DDDのコンテキストはこうだからといってPhoenixのContextもこうあるべきだという議論は恐らく生産的ではないのでやめておいた方がよさそうです。

### Cross context relationship

コンテキストを超えてリレーションが必要になるケース、例えばSalesコンテキストのOrderモデルがAccountsコンテキストのUserモデルにN:1のリレーションを貼りたいというようなケースはどうすればいいのかということがForumでも議論されていましたが、こういう場合はそもそもなぜコンテキストを超える必要があるのかを考える必要があります。

Salesコンテキスト内にUserモデルを定義して同じテーブルを参照することはphoenixでは可能なのでそうすれば解決します。冗長なコードになると考えるかもしれませんが、コンテキストが正しく分割されていればそのモデルで表される知識は別の知識になるので正しいでしょう。逆にコンテキストの分割が間違っていたら同じドメイン知識が複数の箇所で表現されるので抽象化には失敗するでしょう。

将来的にそのコンテキストだけ切り出してUmbrella Appにしたい場合もコンテキストが正しく分割されていればそこまで難しくないし、逆にコンテキストが正しく分割されていなければ切り出すのが難しくなるのでリファクタリングが必要になるはずです。

コンテキストという概念を導入することでよくなるのはたぶんこういうアプリケーションの境界について考える機会が増えるということなのだと思います。
オレオレ抽象化に頼らず正しいバウンダリが設定されているか考えることでよりよい抽象化に繋がるという、DDDの概念のいいとこ取りみたいな感じですがこういう節操なく他のツールのいいとこどりをするのもPhoenixのいいところだと思うので個人的には支持したいです。

オレオレコンテキストが増えそうなのでしばらくはCoC（Convention over Configuration）にのっとってなるべくジェネレータ経由で…

## Umbrella

Elixirデフォルトで提供されている[Umbrella Project](https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-apps.html#umbrella-projects)(傘のように一つのアプリ内に複数のアプリを配置するプロジェクト構成)での抽象化ではプロジェクトをサブモジュールとして扱えるのでmix.exsのDependencyを分割出来たり各サブモジュール単位でリリースを行えるのでより複雑なシステムでは有効になりそうですが、プロジェクト初期には少しオーバースペックでOver Optimizeになるんじゃないか？と手を出しづらいところがありました。

Contextはそれよりはソフトな抽象化でモノリシックなプロジェクト内でコンテキストの切り替えを行うことでUmbrella Projectで行っていたアプリ単位での切り替えをコンテキストの切り替えで簡易的に出来るようにしたという印象です。

Phoenix 1.3では`phx.new`に`--umbrella`オプションが追加され、Umbrella構成でPhoenixプロジェクトを作成することも出来ます。

```bash
$ mix phx.new my_app --umbrella
* creating my_app_umbrella/.gitignore
* creating my_app_umbrella/config/config.exs
* creating my_app_umbrella/config/dev.exs
* creating my_app_umbrella/config/test.exs
* creating my_app_umbrella/config/prod.exs
* creating my_app_umbrella/mix.exs
* creating my_app_umbrella/README.md
* creating my_app_umbrella/apps/my_app_web/config/config.exs
* creating my_app_umbrella/apps/my_app_web/config/dev.exs
* creating my_app_umbrella/apps/my_app_web/config/prod.exs
* creating my_app_umbrella/apps/my_app_web/config/prod.secret.exs
* creating my_app_umbrella/apps/my_app_web/config/test.exs
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/application.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/channels/user_socket.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/endpoint.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/router.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/views/error_helpers.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/views/error_view.ex
* creating my_app_umbrella/apps/my_app_web/mix.exs
* creating my_app_umbrella/apps/my_app_web/README.md
* creating my_app_umbrella/apps/my_app_web/test/test_helper.exs
* creating my_app_umbrella/apps/my_app_web/test/support/channel_case.ex
* creating my_app_umbrella/apps/my_app_web/test/support/conn_case.ex
* creating my_app_umbrella/apps/my_app_web/test/my_app_web/views/error_view_test.exs
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/gettext.ex
* creating my_app_umbrella/apps/my_app_web/priv/gettext/en/LC_MESSAGES/errors.po
* creating my_app_umbrella/apps/my_app_web/priv/gettext/errors.pot
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/controllers/page_controller.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/templates/layout/app.html.eex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/templates/page/index.html.eex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/views/layout_view.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/views/page_view.ex
* creating my_app_umbrella/apps/my_app_web/test/my_app_web/controllers/page_controller_test.exs
* creating my_app_umbrella/apps/my_app_web/test/my_app_web/views/layout_view_test.exs
* creating my_app_umbrella/apps/my_app_web/test/my_app_web/views/page_view_test.exs
* creating my_app_umbrella/apps/my_app_web/.gitignore
* creating my_app_umbrella/apps/my_app_web/assets/brunch-config.js
* creating my_app_umbrella/apps/my_app_web/assets/css/app.css
* creating my_app_umbrella/apps/my_app_web/assets/css/phoenix.css
* creating my_app_umbrella/apps/my_app_web/assets/js/app.js
* creating my_app_umbrella/apps/my_app_web/assets/js/socket.js
* creating my_app_umbrella/apps/my_app_web/assets/package.json
* creating my_app_umbrella/apps/my_app_web/assets/static/robots.txt
* creating my_app_umbrella/apps/my_app_web/assets/static/images/phoenix.png
* creating my_app_umbrella/apps/my_app_web/assets/static/favicon.ico
* creating my_app_umbrella/apps/my_app/config/config.exs
* creating my_app_umbrella/apps/my_app/config/dev.exs
* creating my_app_umbrella/apps/my_app/config/prod.exs
* creating my_app_umbrella/apps/my_app/config/prod.secret.exs
* creating my_app_umbrella/apps/my_app/config/test.exs
* creating my_app_umbrella/apps/my_app/lib/my_app/application.ex
* creating my_app_umbrella/apps/my_app/test/test_helper.exs
* creating my_app_umbrella/apps/my_app/README.md
* creating my_app_umbrella/apps/my_app/mix.exs
* creating my_app_umbrella/apps/my_app/.gitignore
* creating my_app_umbrella/apps/my_app/lib/my_app/repo.ex
* creating my_app_umbrella/apps/my_app/priv/repo/seeds.exs
* creating my_app_umbrella/apps/my_app/test/support/data_case.ex

Fetch and install dependencies? [Yn] n

We are almost there! The following steps are missing:

    $ cd my_app_umbrella
    $ mix deps.get
    $ cd assets && npm install && node node_modules/brunch/bin/brunch build

Then configure your database in apps/my_app/config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server
```

ディレクトリツリーを見てみます

```bash
$ tree
.
├── apps
│   ├── my_app
│   │   ├── config
│   │   │   ├── config.exs
│   │   │   ├── dev.exs
│   │   │   ├── prod.exs
│   │   │   ├── prod.secret.exs
│   │   │   └── test.exs
│   │   ├── lib
│   │   │   └── my_app
│   │   │       ├── application.ex
│   │   │       └── repo.ex
│   │   ├── mix.exs
│   │   ├── priv
│   │   │   └── repo
│   │   │       ├── migrations
│   │   │       └── seeds.exs
│   │   ├── README.md
│   │   └── test
│   │       ├── support
│   │       │   └── data_case.ex
│   │       └── test_helper.exs
│   └── my_app_web
│       ├── assets
│       │   ├── brunch-config.js
│       │   ├── css
│       │   │   ├── app.css
│       │   │   └── phoenix.css
│       │   ├── js
│       │   │   ├── app.js
│       │   │   └── socket.js
│       │   ├── package.json
│       │   ├── static
│       │   │   ├── favicon.ico
│       │   │   ├── images
│       │   │   │   └── phoenix.png
│       │   │   └── robots.txt
│       │   └── vendor
│       ├── config
│       │   ├── config.exs
│       │   ├── dev.exs
│       │   ├── prod.exs
│       │   ├── prod.secret.exs
│       │   └── test.exs
│       ├── lib
│       │   ├── my_app_web
│       │   │   ├── application.ex
│       │   │   ├── channels
│       │   │   │   └── user_socket.ex
│       │   │   ├── controllers
│       │   │   │   └── page_controller.ex
│       │   │   ├── endpoint.ex
│       │   │   ├── gettext.ex
│       │   │   ├── router.ex
│       │   │   ├── templates
│       │   │   │   ├── layout
│       │   │   │   │   └── app.html.eex
│       │   │   │   └── page
│       │   │   │       └── index.html.eex
│       │   │   └── views
│       │   │       ├── error_helpers.ex
│       │   │       ├── error_view.ex
│       │   │       ├── layout_view.ex
│       │   │       └── page_view.ex
│       │   └── my_app_web.ex
│       ├── mix.exs
│       ├── priv
│       │   └── gettext
│       │       ├── en
│       │       │   └── LC_MESSAGES
│       │       │       └── errors.po
│       │       └── errors.pot
│       ├── README.md
│       └── test
│           ├── my_app_web
│           │   ├── channels
│           │   ├── controllers
│           │   │   └── page_controller_test.exs
│           │   └── views
│           │       ├── error_view_test.exs
│           │       ├── layout_view_test.exs
│           │       └── page_view_test.exs
│           ├── support
│           │   ├── channel_case.ex
│           │   └── conn_case.ex
│           └── test_helper.exs
├── config
│   ├── config.exs
│   ├── dev.exs
│   ├── prod.exs
│   └── test.exs
├── mix.exs
└── README.md

37 directories, 56 files
```

出力されたファイルを見てみるとルートディレクトリに`apps`ディレクトリが作成されその中にUmbrella傘下内の一アプリとしてphoenixプロジェクトが作成されているのが分かります。

デフォルトでは`<project>`と`<project>_web`ディレクトリが`apps`内に作成され、`<project>`内にはEctoの依存のみ存在しDBのインターフェースになっていることが分かります。また`<project>_web`ディレクトリは`--no-ecto`オプションで作成した時のようにEctoがdependencyに存在せず素のphoenixプロジェクトになっているのが分かります。

実際にこうしろという規則はありませんがUmbrella Projectのお手本みたいな構成なのでPhoenixプロジェクトが成長してきてUmbrella化したいような場合は参考になるかもしれません。
Umbrellaをいつ使うかについてはPhoenix作者のChris McCordがLonestar Elixir Confで[話しています](https://youtu.be/tMO28ar0lW8?t=28m23s)が、必ずしも全てに当てはまるわけではないけど**Contextがそれ専用のストレージを持つ場合はUmbrella、それ以外なら同一App**内にすると考えている、と言っています。

Microservicesを念頭においているのかもしれませんが、Contextで適切に分割出来ていればUmbrella化からのMicroservices化への道も想像出来るのでモノリス時からContextを使うことは有用だと思います。

## まとめ

Phoenix 1.3でのディレクトリ構造やContext、Umbrella Projectについて確認しました。

Phoenix 1.2まではRailsライクな構成が新規参入のしやすさに一役買っていたところもあると思いますがOOでないElixirにそうなる必然性はないといえばないので、Umbrellaプロジェクトのオーバースペック感も否めなかったところにPhoenix 1.3のContextという概念は調度いい落とし所を作ったなという印象です。

※2017-06-27現在Phoenix 1.3-rc2で正式版はリリースされていないのでこの記事の内容は変わる可能性があります