(追記)2022/10/02 Phoenix1.6に合わせて更新し、大幅に加筆しました
今回も割と地味目な話です。Phoenix1.3の基本的な話です。
本記事ではGenerator(phx.gen.html)の吐き出すコードを確認することで、Phoenixの基本的な仕組みを追ってみたいと思います。本記事を読むことでPhoenixによるCRUDアプリの骨組みを理解することができます。(多分)
【過去記事】
-
LiveView関連
東京電力電力供給状況監視 - Phoenix LiveView
Phoenix LiveView と キーボードイベント - Qiita
Phoenix LiveView の JavaScript Hook - Qiita
Phoenix LiveViewの基本設定 - Qiita
Phoenix1.6の基本的な仕組み - Qiita -
認証関連
Phoenix 認証システム - mix phx.gen.auth
Elixir/Phoenix のシンプル認証 auth_plug -
Ecto関連
Elixir Ecto チュートリアル - Qiita
Elixir Ecto のまとめ - Qiita
Elixir Ecto Association - Oiita
1.Generator - phx.gen.html
プロジェクトを作成し、DBを初期化します。
mix phx.new people
cd people
mix ecto.setup
generatorのコマンドphx.gen.htmlを以下のように実行します。このgeneratorが必要なmoduleを全て生成してくれます。generatorを使わず手作業でmoduleを作るときも、generatorで生成したものを参考にすれば良いわけです。
mix phx.gen.html Users User users first_name:string last_name:string age:integer
引数の意味は下記の表の通りです。このコマンドにより、それぞれ表にあるようなDB関連のElixir moduleを自動生成します。
引数 | 値 | 意味 | 補足 |
---|---|---|---|
1 | Users | context module | |
2 | User | schema module | |
3 | users | schema table name | schema moduleの複数形 |
以下自動生成されたファイルに日本語でコメントを加えていきます。
2.DB関連モジュール
2-1.context module
ドキュメントでは、context はresourceのAPI boundaryを提供するElixir moduleです、と述べられています。つまり context は、schemas や database に対して複数の操作が必要な時に、それらを一つの関数にまとめて、シンプルなインターフェースを提供してくれるmoduleです。ラッパー関数の集まりです。 controllerからdatabaseにアクセスするときなどは、関数を一発呼ぶだけで、複雑な操作はこのmoduleで行ってくれます。
(注意)users.exは自動生成されたものですが、長くなるのでコメントはカットしてあります。 <=コメントもそのまま掲載してます。
defmodule People.Users do
@moduledoc """
The Users context.
"""
import Ecto.Query, warn: false
alias People.Repo
alias People.Users.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{data: %User{}}
"""
def change_user(%User{} = user, attrs \\ %{}) do
User.changeset(user, attrs)
end
end
2-2.schema module
Schema は DB テーブルを Elixir struct に map するために使われます。 テーブルの定義をstructに反映したものになります。changeset の定義も行われます。schemaとchangesetについては以下の過去記事を参照してください。
Elixir Ecto チュートリアル - Qiita
Elixir Ecto のまとめ - Qiita
defmodule People.Users.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :age, :integer
field :first_name, :string
field :last_name, :string
timestamps() #time関連の項目が自動で追加
end
#schemaとchangesetはペアで定義されます。
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:first_name, :last_name, :age])
|> validate_required([:first_name, :last_name, :age])
end
end
2-3.schema table
schema table の定義ファイルです。
defmodule People.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do # schema tableのcreateの定義
add :first_name, :string
add :last_name, :string
add :age, :integer
timestamps() #time関連の項目が自動で追加
end
end
end
以下のコマンドで実際にschema tableを作成します。
mix ecto.migrate
3.routerモジュール
Router はクライアントからのリクエストを、パスを基に、適切なController(の関数)に振り分ける役割を果たします。
phx.gen.htmlコマンドで、router.exに以下の一行を追加するようガイドされます。手動で追加します。
defmodule PeopleWeb.Router do
use PeopleWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", PeopleWeb do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
resources "/users", UserController # これを追加する
end
# Other scopes may use custom stacks.
# scope "/api", PeopleWeb do
# pipe_through :api
# end
end
resources "/users", UserController行の追加の結果を mix phx.routes コマンドで確認します。==> Routing
---
user_path GET /users PeopleWeb.UserController :index
user_path GET /users/:id/edit PeopleWeb.UserController :edit
user_path GET /users/new PeopleWeb.UserController :new
user_path GET /users/:id PeopleWeb.UserController :show
user_path POST /users PeopleWeb.UserController :create
user_path PATCH /users/:id PeopleWeb.UserController :update
PUT /users/:id PeopleWeb.UserController :update
user_path DELETE /users/:id PeopleWeb.UserController :delete
---
4.controllerモジュールとViewモジュール
phx.gen.htmlは、Controller と View のElixir moduleも自動生成します。
4-1.Controller
Controller はクライアントからのリクエストを処理する関数の集まりです。以下のソースコードに説明コメントを追加しました。
※以下、ContorollerからTemplateに渡されるusersやchangeset等のデータを明確にするため(4-1)等の番号を振ってあります。
defmodule PeopleWeb.UserController do
use PeopleWeb, :controller
alias People.Users
alias People.Users.User
# トップページ、user一覧を表示する
def index(conn, _params) do
users = Users.list_users()
render(conn, "index.html", users: users)
end
# user作成画面を表示する
def new(conn, _params) do
changeset = Users.change_user(%User{})
render(conn, "new.html", changeset: changeset)
end
# user作成を実行する。
def create(conn, %{"user" => user_params}) do
case Users.create_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: user_path(conn, :show, user))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
# idで指定されたuserを表示する
def show(conn, %{"id" => id}) do
user = Users.get_user!(id)
render(conn, "show.html", user: user)
end
# idで指定されたuserの編集画面を表示する
def edit(conn, %{"id" => id}) do
user = Users.get_user!(id)
changeset = Users.change_user(user)
render(conn, "edit.html", user: user, changeset: changeset)
end
# user編集を実行する
def update(conn, %{"id" => id, "user" => user_params}) do
user = Users.get_user!(id)
case Users.update_user(user, user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User updated successfully.")
|> redirect(to: user_path(conn, :show, user))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", user: user, changeset: changeset)
end
end
# userを削除する
def delete(conn, %{"id" => id}) do
user = Users.get_user!(id)
{:ok, _user} = Users.delete_user(user)
conn
|> put_flash(:info, "User deleted successfully.")
|> redirect(to: user_path(conn, :index))
end
end
(1) Rendering - render(conn, template, assigns)
assignsを使ってtemplateを描画します。
render関数の使用例です。
defmodule MyAppWeb.UserController do
use Phoenix.Controller
def show(conn, _params) do
render(conn, "show.html", message: "Hello")
end
end
(2) Redirection - redirect(conn, opts)
与えられたURLへのredirect responseを送ります。
redirect(conn, to: "/login")
redirect(conn, external: "https://elixir-lang.org")
(3) Flash messages - put_flash(conn, key, message)
conn に Flash messages を保存します。
iex> conn = put_flash(conn, :info, "Welcome Back!")
iex >get_flash(conn, :info)
"Welcome Back!"
ちなみにuser_controller.exで保存されたFlash messagesはapp.html.heexの中で以下のように表示されています。
<main class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<%= @inner_content %>
</main>
4-2.View
defmodule PeopleWeb.UserView do
use PeopleWeb, :view
end
5.templatesファイル
phx.gen.htmlは以下のtemplatesファイルも自動生成します。
Phoenixの基本的なことですが、templateがdataを受け取る方法は、conn の assigns mapを通してです。
"assigns" storageはplugs 間同士で値を共有するのに使われます。
- assign(conn, key, value) でconn のassignsに追加します。明示的に使われることはあまりないようですが。
assigns storage はmapです。
iex> conn.assigns[:hello]
nil
iex> conn = assign(conn, :hello, :world)
iex> conn.assigns[:hello]
:world
templateの中でassignsを参照するには @ ショートカットが用意されています。
render(conn, "show.html", username: "joe")
これはtemplateでは、以下のように参照されます。
<p>user: <%= @username %></p>
5-1.index
トップページです。user一覧を表示します。(4-1)のusersがこのtemplateで@usersで参照されています。
<h1>Listing Users</h1>
<table>
<thead>
<tr>
<th>First name</th>
<th>Last name</th>
<th>Age</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for user <- @users do %>
<tr>
<td><%= user.first_name %></td>
<td><%= user.last_name %></td>
<td><%= user.age %></td>
<td>
<span><%= link "Show", to: Routes.user_path(@conn, :show, user) %></span>
<span><%= link "Edit", to: Routes.user_path(@conn, :edit, user) %></span>
<span><%= link "Delete", to: Routes.user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"] %></span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span><%= link "New User", to: Routes.user_path(@conn, :new) %></span>
user_path()は Path Helpers と呼ばれ、Routingのパスを生成してくれます。==> Routing / Path Helpers
ここでちょっとPath Helpersを試してみましょう。
$iex -S mix phx.server
iex(3)> alias PeopleWeb.Router.Helpers
PeopleWeb.Router.Helpers
iex(4)> Helpers.user_path(PeopleWeb.Endpoint, :index)
"/users"
iex(5)> Helpers.user_path(PeopleWeb.Endpoint, :edit, %People.Users.User{id: 1})
"/users/1/edit"
iex(6)> Helpers.user_path(PeopleWeb.Endpoint, :show, %People.Users.User{id: 1})
"/users/1"
iex(7)> Helpers.user_path(PeopleWeb.Endpoint, :delete, %People.Users.User{id: 1})
"/users/1"
5-2.show
個別のuserを表示します。
<h1>Show User</h1>
<ul>
<li>
<strong>First name:</strong>
<%= @user.first_name %>
</li>
<li>
<strong>Last name:</strong>
<%= @user.last_name %>
</li>
<li>
<strong>Age:</strong>
<%= @user.age %>
</li>
</ul>
<span><%= link "Edit", to: Routes.user_path(@conn, :edit, @user) %></span> |
<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>
5-3.new
user作成画面を表示します。
assignsには(4-2) (4-3)のchangesetが含まれている。assignsにaction:user_path(@conn, :create) を追加して、form.htmlをrenderします。
<h1>New User</h1>
<%= render "form.html", Map.put(assigns, :action, Routes.user_path(@conn, :create)) %>
<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>
またここで入力された値はcontorollerのcreateの"user"に渡されます。
def create(conn, %{"user" => user_params}) do
ちなみにassignsにactionを追加するにはMap.putを使っています。
Map.put(map, key, value)
Puts the given value under key in map
5-4.edit
user編集画面を表示します。
assignsには (4-5) (4-6) のchangesetが含まれている。assignsにaction:user_path(@conn, :update, @user) を追加して、form.htmlをrenderします。
<h1>Edit User</h1>
<%= render "form.html", Map.put(assigns, :action, Routes.user_path(@conn, :update, @user)) %>
<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>
またここで入力された値はcontorollerのupdateの"user"に渡されます。
def update(conn, %{"id" => id, "user" => user_params}) do
5-5.form
newとeditの中身です。両者では渡されてくるactionの値だけが違います。
changesetがRepo.[insert,update]で作成された場合は、「Oops,...」の警告文を出します。
<.form let={f} for={@changeset} action={@action}>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :first_name %>
<%= text_input f, :first_name %>
<%= error_tag f, :first_name %>
<%= label f, :last_name %>
<%= text_input f, :last_name %>
<%= error_tag f, :last_name %>
<%= label f, :age %>
<%= number_input f, :age %>
<%= error_tag f, :age %>
<div>
<%= submit "Save" %>
</div>
</.form>
以下少し長くなりますが、formについての説明が続きます。
(1) Phoenix.HTML.Form について
(A) form_for(form_data, action, options \ [], fun)
- form builder と 無名関数からformタグを生成する関数
<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
Name: <%= text_input f, :name %>
<% end %>
(B) Phoenix.LiveView integration
しかしPhoenix.LiveViewとの統合でform_forは使わないよう案内されています。
<%= form_for @changeset, url, opts, fn f -> %>
<%= text_input f, :name %>
<% end %>
Phoenix.LiveView.Helpers を importして以下のように書き換えるべき。
<.form let={f} for={@changeset}>
<%= text_input f, :name %>
</.form>
(C) label(form, field)
- field のラベルタグを生成する関数
- form は Phoenix.HTML.Form (emitted by form_for) または atom
# Assuming form contains a User schema
label(form, :name, "Name")
#=> <label for="user_name">Name</label>
label(:user, :email, "Email")
#=> <label for="user_email">Email</label>
(D) text_input(form, field, opts \ [])
- text input を生成する関数
- form は Phoenix.HTML.Form (emitted by form_for) または atom
# Assuming form contains a User schema
text_input(form, :name)
#=> <input id="user_name" name="user[name]" type="text" value="">
text_input(:user, :name)
#=> <input id="user_name" name="user[name]" type="text" value="">
error_tagはドキュメントにはありませんでしたが、label や text_inputと同等と思われます。
(2) changeset.actionについて
changeset.actionにはchangesetを作成した"action"名が入るようです。
- changeset = Users.change_user(%User{}) の場合はchangeset.action = nil となります
- {:error, changeset} = Repo.insert(changeset) の場合はchangeset.action = insertとなります。
- {:error, changeset} = Repo.update(changeset) の場合はchangeset.action = updateとなります。
(3) Repo.insert のエラー時に作られるchangeset
またUserの新規作成時にlast_nameを空白のまま Repo.insert すると以下のようなchangesetが返されます。
#Ecto.Changeset<
action: :insert,
changes: %{age: 14, first_name: "taro"},
errors: [last_name: {"can't be blank", [validation: :required]}],
data: #People.Users.User<>,
valid?: false
formの error_tag には changeset.errors の内容が表示されます。
他方、First name と Age の値は changeset.changes に保持され、それぞれのfieldに表示されます。
(4) new 画面に渡されるchangeset
changeset = Users.change_user(%User{}) で作られたchangesetが, new 画面に渡されますが、以下のような内容になります。
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [
first_name: {"can't be blank", [validation: :required]},
last_name: {"can't be blank", [validation: :required]},
age: {"can't be blank", [validation: :required]}
],
data: #People.Users.User<>,
valid?: false
このchangesetはerrorsを含みますが、actionがnilなのでformにはエラー表示はありません。 ドキュメントの「A note on :errors」を参照してください。
今回は以上です。
■ Elixir/Phoenixの基礎についてまとめた過去記事
Elixir Ecto チュートリアル - Qiita
Elixir Ecto のまとめ - Qiita
Elixir Ecto Association - Qiita
Phoenix1.6の基本的な仕組み - Qiita
Phoenixのログイン管理のためのSessionの使い方 - Qiita
Phoenix1.3のUserアカウントとSession - Qiita
Phoenix1.3+Guardian1.0でJWT - Qiita