はじめに
ひとり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で現在ログインしているユーザーを取得することができます
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
<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の配下に置きます
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でも参照できるようにします
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
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
<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側でリレーションを組んでいきます
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を読み込みます
defmodule LiveLogger.Loggers do
...
def list_maps do
Map
|> preload(:user)
|> Repo.all()
end
...
end
idの代わりにuserのemailを表示するようにして完成です
<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>
記事では以下のことができました
プロジェクトの作成
認証機能の追加
CRUD画面の追加
リレーションの作成
次はphx.gen.liveで生成されたコードを解説します
Code