LoginSignup
9
4

More than 1 year has passed since last update.

Phoenix1.6の基本的な仕組み

Last updated at Posted at 2018-09-14

(追記)2022/10/02 Phoenix1.6に合わせて更新し、大幅に加筆しました

 今回も割と地味目な話です。Phoenix1.3の基本的な話です。

 本記事ではGenerator(phx.gen.html)の吐き出すコードを確認することで、Phoenixの基本的な仕組みを追ってみたいと思います。本記事を読むことでPhoenixによるCRUDアプリの骨組みを理解することができます。(多分)

【過去記事】

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は自動生成されたものですが、長くなるのでコメントはカットしてあります。 <=コメントもそのまま掲載してます。

lib/people/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

lib/people/users/user.ex
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 の定義ファイルです。

priv/repo/migrations/20221002050538_create_users.exs
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に以下の一行を追加するようガイドされます。手動で追加します。

lib/people_web/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は、ControllerView のElixir moduleも自動生成します。

4-1.Controller

 Controller はクライアントからのリクエストを処理する関数の集まりです。以下のソースコードに説明コメントを追加しました。

※以下、ContorollerからTemplateに渡されるusersやchangeset等のデータを明確にするため(4-1)等の番号を振ってあります。

lib/people_web/controllers/user_controller.ex
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の中で以下のように表示されています。

lib/people_web/templates/layout/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

lib/people_web/views/user_view.ex
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で参照されています。

lib/people_web/templates/user/index.html.heex
<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を表示します。

lib/people_web/templates/user/show.html.heex
<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します。

lib/people_web/templates/user/new.html.heex
<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します。

lib/people_web/templates/user/edit.html.heex
<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,...」の警告文を出します。

lib/people_web/templates/user/form.html.heex
<.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 について

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 のラベルタグを生成する関数
  • formPhoenix.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 を生成する関数
  • formPhoenix.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に表示されます。

image.png

(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

9
4
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
9
4