LoginSignup
8
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-11-30

はじめに

ひとりLiveView Advent Calendar の1日目の記事です

この記事はElixir Conf US 2021発表したシステムの構築と関連技術の解説を目的とした記事です

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

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

プロジェクトの作成

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

  • 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
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
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
* 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
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>

2モデル間を関連付ける

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を表示するようにして完成です

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

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

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

Code

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2