Phoenixで作るGPS Logging System 1 プロジェクト作成

# はじめに
[ひとりLiveView Advent Calendar](https://qiita.com/advent-calendar/2021/liveview_alone) の１日目の記事です

この記事は[Elixir Conf US 2021](https://www.elixirconf.com/)の[発表](https://www.youtube.com/watch?v=vHNqZmLLTj4)したシステムの構築と関連技術の解説を目的とした記事です

今回は以下の４つのことをやっていきます

- プロジェクトの作成
- phx.gen.auth による認証機能の追加
- phx.gen.live によるCRUD画面の作成
- 上記２つで作成したモデル間のリレーションを組む

# プロジェクトの作成
以下の環境で新しくプロジェクトを作成します

- Elixir 1.12.3
- Erlang 24.1.5
- Phoenix 1.6.2

```
mix phx.new live_logger
cd live_logger
mix setup
```

Phoenix 1.6ではphx.gen.auth, LiveViewはデフォルトで入るようになり、
またwebpackからesbuildに変わったため 最初のnpm installをする必要がなくなりました。

# phx.gen.auth による認証機能の追加

phx.gen.authはRailsのDeviseに似た認証機能を追加するライブラリで1.6でPhoenix本体にマージされました
機能としては以下のようなものがあります

* 登録、ログイン、ログアウト
* パスワード・メールアドレスの変更
* 確認メールの送信
* アクセスコントロール

いつものコマンドを実行して

```
mix phx.gen.auth Accounts User users
```

```bash
Compiling 14 files (.ex)
Generated live_logger app
* creating priv/repo/migrations/20211118100725_create_users_auth_tables.exs
* creating lib/live_logger/accounts/user_notifier.ex
* creating lib/live_logger/accounts/user.ex
* creating lib/live_logger/accounts/user_token.ex
* creating lib/live_logger_web/controllers/user_auth.ex
* creating test/live_logger_web/controllers/user_auth_test.exs
* creating lib/live_logger_web/views/user_confirmation_view.ex
* creating lib/live_logger_web/templates/user_confirmation/new.html.heex
* creating lib/live_logger_web/templates/user_confirmation/edit.html.heex
* creating lib/live_logger_web/controllers/user_confirmation_controller.ex
* creating test/live_logger_web/controllers/user_confirmation_controller_test.exs
* creating lib/live_logger_web/templates/layout/_user_menu.html.heex
* creating lib/live_logger_web/templates/user_registration/new.html.heex
* creating lib/live_logger_web/controllers/user_registration_controller.ex
* creating test/live_logger_web/controllers/user_registration_controller_test.exs
* creating lib/live_logger_web/views/user_registration_view.ex
* creating lib/live_logger_web/views/user_reset_password_view.ex
* creating lib/live_logger_web/controllers/user_reset_password_controller.ex
* creating test/live_logger_web/controllers/user_reset_password_controller_test.exs
* creating lib/live_logger_web/templates/user_reset_password/edit.html.heex
* creating lib/live_logger_web/templates/user_reset_password/new.html.heex
* creating lib/live_logger_web/views/user_session_view.ex
* creating lib/live_logger_web/controllers/user_session_controller.ex
* creating test/live_logger_web/controllers/user_session_controller_test.exs
* creating lib/live_logger_web/templates/user_session/new.html.heex
* creating lib/live_logger_web/views/user_settings_view.ex
* creating lib/live_logger_web/templates/user_settings/edit.html.heex
* creating lib/live_logger_web/controllers/user_settings_controller.ex
* creating test/live_logger_web/controllers/user_settings_controller_test.exs
* creating lib/live_logger/accounts.ex
* injecting lib/live_logger/accounts.ex
* creating test/live_logger/accounts_test.exs
* injecting test/live_logger/accounts_test.exs
* creating test/support/fixtures/accounts_fixtures.ex
* injecting test/support/fixtures/accounts_fixtures.ex
* injecting test/support/conn_case.ex
* injecting config/test.exs
* injecting mix.exs
* injecting lib/live_logger_web/router.ex
* injecting lib/live_logger_web/router.ex - imports
* injecting lib/live_logger_web/router.ex - plug
* injecting lib/live_logger_web/templates/layout/root.html.heex

Please re-fetch your dependencies with the following command:

    $ mix deps.get

Remember to update your repository by running migrations:

    $ mix ecto.migrate

Once you are ready, visit "/users/register"
to create your account and then access to "/dev/mailbox" to
see the account confirmation email.

```

ログの通りにコマンドを実行します

```
mix deps.get
mix ecto.migrate
```

phx.gen.authをしたことで browser pipelineに fetch_current_userが追加され connを使用する箇所全てで@current_userで現在ログインしているユーザーを取得することができます

```lib/live_logger_web/router.ex
defmodule LiveLoggerWeb.Router do
  use LiveLoggerWeb, :router

  import LiveLoggerWeb.UserAuth

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {LiveLoggerWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :fetch_current_user # <- これ
  end
...
end
```

```leex:lib/live_logger_view_web/templates/layout/_user_menu.html.heex
<ul>
<%= if @current_user do %>
  <li><%= @current_user.email %></li>
  <li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
  <li><%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
<% else %>
  <li><%= link "Register", to: Routes.user_registration_path(@conn, :new) %></li>
  <li><%= link "Log in", to: Routes.user_session_path(@conn, :new) %></li>
<% end %>
</ul>
```


# phx.gen.live によるCRUD画面の作成
認証機能ができたので実際に管理するデータのCRUD画面を作成します
このプロジェクトはGPSログをMapという単位でまとめたいので以下のコマンドを実行します
外部キーに references + table名を指定することで一緒にuserとmapの複合indexを作ってくれます

```
mix phx.gen.live Loggers Map maps name:string description:string user_id:references:users
```

```bash
* creating lib/live_logger_web/live/map_live/show.ex
* creating lib/live_logger_web/live/map_live/index.ex
* creating lib/live_logger_web/live/map_live/form_component.ex
* creating lib/live_logger_web/live/map_live/form_component.html.heex
* creating lib/live_logger_web/live/map_live/index.html.heex
* creating lib/live_logger_web/live/map_live/show.html.heex
* creating test/live_logger_web/live/map_live_test.exs
* creating lib/live_logger_web/live/modal_component.ex
* creating lib/live_logger_web/live/live_helpers.ex
* creating lib/live_logger/loggers/map.ex
* creating priv/repo/migrations/20211118095356_create_maps.exs
* creating lib/live_logger/loggers.ex
* injecting lib/live_logger/loggers.ex
* creating test/live_logger/loggers_test.exs
* injecting test/live_logger/loggers_test.exs
* creating test/support/fixtures/loggers_fixtures.ex
* injecting test/support/fixtures/loggers_fixtures.ex
* injecting lib/live_logger_web.ex

Add the live routes to your browser scope in lib/live_logger_web/router.ex:

    live "/maps", MapLive.Index, :index
    live "/maps/new", MapLive.Index, :new
    live "/maps/:id/edit", MapLive.Index, :edit

    live "/maps/:id", MapLive.Show, :show
    live "/maps/:id/show/edit", MapLive.Show, :edit


Remember to update your repository by running migrations:

    $ mix ecto.migrate

```
ログに出てきたリンクをrouterに追加します
外部に公開しないのでログインを要求する `:require_authenticated_user` をpipe_throughするscopeの配下に置きます


```lib/live_logger_web/router.ex
defmodule LiveLoggerWeb.Router do
  use LiveLoggerWeb, :router

  ...
  scope "/", LiveLoggerWeb do
    pipe_through [:browser, :require_authenticated_user]

    get "/users/settings", UserSettingsController, :edit
    put "/users/settings", UserSettingsController, :update
    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
    # 以下追加
    live "/maps", MapLive.Index, :index
    live "/maps/new", MapLive.Index, :new
    live "/maps/:id/edit", MapLive.Index, :edit

    live "/maps/:id", MapLive.Show, :show
    live "/maps/:id/show/edit", MapLive.Show, :edit
  end
  ...
end
```
以下のコマンドでmigrationファイルの内容をDBに反映させます

```
mix ecto.migrate
```

controllerを使う箇所では@current_userでログイン中のユーザーを取得できますが、
LiveViewではconnではなくsessionの内容しか参照できないため、
ログインした際にsessionにuser_idを入れてLiveViewでも参照できるようにします

```lib/live_map_view_web/controllers/user_auth.ex
defmodule LiveLoggerWeb.UserAuth do
  import Plug.Conn
  import Phoenix.Controller
  ...

  def log_in_user(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
    user_return_to = get_session(conn, :user_return_to)

    conn
    |> renew_session()
    |> put_session(:user_token, token)
    |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
    |> put_session(:user_id, user.id) # ここ追加
    |> maybe_write_remember_me_cookie(token, params)
    |> redirect(to: user_return_to || signed_in_path(conn))
  end
end
```

```lib/live_logger_web/live/map_live/index.ex
defmodule LiveLoggerWeb.MapLive.Index do
  use LiveLoggerWeb, :live_view

  alias LiveLogger.Loggers
  alias LiveLogger.Loggers.Map

  @impl true
  # sessionからuser_idをパターンマッチするように変更
  def mount(_params, %{"user_id" => user_id}, socket) do 
    {
      :ok,
      socket
      |> assign(:maps, list_maps())
      |> assign(:user_id, user_id) # 追加
    }
  end

  ...
  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Map")
    |> assign(:map, %Map{user_id: socket.assigns.user_id}) # changesetの初期値にuser_id追加
  end
  ...
end
```

```leex:lib/live_logger_web/live/map_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="map-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <%= label f, :name %>
    <%= text_input f, :name %>
    <%= error_tag f, :name %>
  
    <%= label f, :description %>
    <%= text_input f, :description %>
    <%= error_tag f, :description %>
  
    <%= hidden_input f, :user_id %> <!--- 追加 --->
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
```

# ２モデル間を関連付ける
Model側でリレーションを組んでいきます

```lib/live_logger/loggers/map.ex
defmodule LiveLogger.Loggers.Map do
  use Ecto.Schema
  import Ecto.Changeset

  schema "maps" do
    field :description, :string
    field :name, :string

    belongs_to :user, LiveLogger.Accounts.User # user_idを削除してこれを追加
    timestamps()
  end

  @doc false
  def changeset(map, attrs) do
    map
    |> cast(attrs, [:name, :description, :user_id]) # user_idを追加
    |> validate_required([:name, :description, :user_id]) # user_idを追加
  end
end
```
PhoenixのORMであるEctoでは明示的にpreloadやjoinしないと関連先は取得できないので
preloadを実行して関連付けているUserを読み込みます

```lib/live_logger/loggers.ex
defmodule LiveLogger.Loggers do
  ...
  def list_maps do
    Map
    |> preload(:user)
    |> Repo.all()
  end
  ...
end
```
idの代わりにuserのemailを表示するようにして完成です

```leex:lib/live_logger_web/live/map_live/index.html.heex
<h1>Listing Maps</h1>
...
<table>
  <thead>
    <tr>
      <th>User</th>
      <th>Name</th>
      <th>Description</th>

      <th></th>
    </tr>
  </thead>
  <tbody id="maps">
    <%= for map <- @maps do %>
      <tr id={"map-#{map.id}"}>
        <td><%= map.user.email %></td> <!--- 追加 --->
        <td><%= map.name %></td>
        <td><%= map.description %></td>
        <td>...</td>
      </tr>
    <% end %>
  </tbody>
</table>
```

![スクリーンショット 2021-11-19 1.25.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/58f72768-92b1-14be-b678-926461739b24.png)

記事では以下のことができました
プロジェクトの作成
認証機能の追加
CRUD画面の追加
リレーションの作成

次はphx.gen.liveで生成されたコードを解説します

# Code
https://github.com/thehaigo/live_logger