mmag

ハマったことメモなど

Phoenix v1.3からのディレクトリ構成をもう一度調べる

はいこんにちは。

Phoenix v1.3.0-rc.0がリリースされました。前リリースからの大きな変更として、Phoenixプロジェクトのディレクトリ構成がガラッと変わります。新たな構成に対応したジェネレータは、phoenix.*ではなくphx.*というタスクになっています。ついで感がありますが、phoenix.serverphoenix.routesもdeprecatedになり、phx.serverphx.routesになっていくようです。

github.com

以前v1.3以降でどんな構成になるのか調べたのですが、改めて調べておきましょう。

joe-noh.hatenablog.com

phx.new

まずはv1.2以前のphoenix.newと、v1.3から入るphx.newでつくられるプロジェクト構成を比較してみます。Phoenixリポジトリをcloneして、installerディレクトリの中で以下のコマンドを発行。Phoenixのリビジョンは1.3.0-rc.0タグのついた4d608bfです。

$ mix phoenix.new app_with_phoenix_new
$ mix phx.new app_with_phx_new --dev

phoenix.new--devつけたら--dev projects must be generated inside Phoenix directoryと怒られたのですが、ディレクトリ構成が見たいだけなので--dev無しで。つくられたディレクトリをtreeで見るとこんな形式。一部省略してあります。

app_with_phoenix_new
├── README.md
├── config
├── lib
│   ├── app_with_phoenix_new
│   │   ├── endpoint.ex
│   │   └── repo.ex
│   └── app_with_phoenix_new.ex
├── mix.exs
├── priv
│   ├── gettext
│   └── repo
├── test
│   ├── channels
│   ├── controllers
│   ├── models
│   ├── support
│   ├── test_helper.exs
│   └── views
└── web
    ├── channels
    ├── controllers
    ├── gettext.ex
    ├── models
    ├── router.ex
    ├── static
    ├── templates
    ├── views
    └── web.ex
app_with_phx_new
├── README.md
├── assets
│   ├── brunch-config.js
│   ├── css
│   ├── js
│   ├── package.json
│   ├── static
│   └── vendor
├── config
├── lib
│   └── app_with_phx_new
│       ├── application.ex
│       ├── repo.ex
│       └── web
│           ├── channels
│           ├── controllers
│           ├── endpoint.ex
│           ├── gettext.ex
│           ├── router.ex
│           ├── templates
│           ├── views
│           └── web.ex
├── mix.exs
├── priv
│   ├── gettext
│   └── repo
└── test
    ├── support
    ├── test_helper.exs
    └── web
        ├── channels
        ├── controllers
        └── views

以前書いた

  • weblib配下に入る
  • modelという概念がなくなる

はそのままのようですが、加えて、これまでweb/static配下にあったcssjsや、package.jsonbrunch-config.jsがまるっとトップレベルのassetsに移っています。見慣れないapplication.exというファイルがありますが、これは上の例だとapp_with_phoenix_new.exに当たるもので、名称が変更されるようです。全体的にディレクトリ構成とモジュール名の対応関係がキチンとしていて、生成されたlib/app_with_phx_new/web/page_controller.exではAppWithPhxNew.Web.PageControllerモジュールが定義されていました。

defmodule AppWithPhxNew.Web.PageController do
  use AppWithPhxNew.Web, :controller

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

ただ、よく見るとweb.exが入る場所が一段深くなっていて、lib/app_with_phx_new/web/web.exとなっています。ここはモジュール名とファイル配置の対応が取れていませんが、Elixir ForumのJoséのコメントによると、意図したものであるようです。

phx.gen.json

次はphx.gen.jsonを試してみます。まずはhelpから。

$ mix help phx.gen.json

                                mix phx.gen.json

Generates controller, views, and context for an JSON resource.

    mix phx.gen.json Accounts User users name:string age:integer

The first argument is the context name followed by the schema module and its
plural name (used for resources and schema).

The above generated resource will add the following files to lib/your_app:

  • a context module in accounts.ex, serving as the API boundary to the
    resource
  • a schema in accounts/user.ex, with an accounts_users table
  • a view in web/views/user_view.ex
  • a controller in web/controllers/user_controller.ex
  • default CRUD templates in web/templates/user

As well as a migration file for the repository and test files for generated
context and controller features.

## Schema table name

By deault, the schema table name will be the plural name, namespaced by the
context name. You can customize this value by providing the --table option to
the generator.

Read the documentation for phx.gen.schema for more information on attributes
and supported options.

Location: _build/dev/lib/phoenix/ebin

これまではmix phoenix.gen.json User users name:string age:integerのように、スキーマのモジュール名とテーブル名、テーブル構造を引数に与えていましたが、Accountsという第一引数が増えています。helpにある例をそのまま実行してみます。

$ mix phx.gen.json Accounts User users name:string age:integer
* creating lib/app_with_phx_new/web/controllers/user_controller.ex
* creating lib/app_with_phx_new/web/views/user_view.ex
* creating test/web/controllers/user_controller_test.exs
* creating lib/app_with_phx_new/web/views/changeset_view.ex
* creating lib/app_with_phx_new/web/controllers/fallback_controller.ex
* creating test/accounts_test.exs
* creating lib/app_with_phx_new/accounts/user.ex
* creating priv/repo/migrations/20170302235526_create_accounts_user.exs

Add the resource to your api scope in lib/app_with_phx_new/web/router.ex:

    resources "/users", UserController, except: [:new, :edit]


Remember to update your repository by running migrations:

    $ mix ecto.migrate

引数に与えたAccountsがどこに効いているかというと、lib/app_with_phx_new/accounts/user.exのようにuserのひとつ上の名前空間になっています。テーブル名も、特に指定しないとaccounts_usersとなるようです。また、コンソールに出ていませんが、しれっとlib/app_with_phx_new/accounts/accounts.exも作られていました。このAccountsモジュールはいわゆる「境界づけられたコンテキスト」のひとつで、他のモジュール、例えばUserControllerは、Userモジュールに定義した関数を使ったり、Repo.all(User)のようなことはせず、このAccountsモジュール経由でユーザの操作を行うようにしましょうね、という意図があるようです。生成されたファイルの中を見ても、UserControllerindexアクションでは、Accounts.list_usersを使ってユーザ一覧を得るようになっていました。Userモジュールはスキーマ定義があるだけでほぼ空っぽで、テストもUserではなくAccountsモジュールのものが作られています。

ただ、生成されたAccountsモジュールの中に以下の関数が定義されており、少々やりすぎではという印象でした。Accountsモジュールが太っていきそうなので、自分ならこれは今まで通りUserモジュールに移します。

def AppWithPhxNew.Accounts do
  ...

  defp user_changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:name, :age])
    |> validate_required([:name, :age])
  end
end
defmodule AppWithPhxNew.Accounts.User do
  use Ecto.Schema
  
  schema "accounts_users" do
    field :name, :string
    field :age, :integer

    timestamps()
  end

  # ここにchangeset/2を書きたい
end

なお、lib/app_with_phx_new/web/controllers/fallback_controller.exという馴染みのないものがありますが、ちょっとここでは深掘りしません。

まとめ

ざっくりまとめると、

  • PhoenixはElixirアプリケーションのWebフロントとして考える
  • テーブルと直接対応していないコンテキストモジュールをAPIの境界とし、ここにロジックを書く

ということをフレームワークが緩く強制するということのようです。