Phoenix1.6の基本的な仕組み

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

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

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


**【過去記事】**
* LiveView関連
[東京電力電力供給状況監視 - Phoenix LiveView](https://qiita.com/sand/items/ecac700bc3e32f106279)
[Phoenix LiveView と キーボードイベント - Qiita](https://qiita.com/sand/items/a72a74ce7885a80eb969)
[Phoenix LiveView の JavaScript Hook - Qiita](https://qiita.com/sand/items/66049160bf5309e272ea)
[Phoenix LiveViewの基本設定 - Qiita](https://qiita.com/sand/items/5aea96852ceb2981021b)
[Phoenix1.6の基本的な仕組み - Qiita](https://qiita.com/sand/items/f42dc35c47589eab687e)

* 認証関連
[Phoenix 認証システム - mix phx.gen.auth](https://qiita.com/sand/items/dda77cc3da3febf3c286)
[Elixir/Phoenix のシンプル認証 auth_plug](https://qiita.com/sand/items/160af41bd019e9c9f388)

* Ecto関連
[Elixir Ecto チュートリアル - Qiita](https://qiita.com/sand/items/0861ad7a16f7ac612f41)
[Elixir Ecto のまとめ - Qiita](https://qiita.com/sand/items/71d0b35d74a4781f3564)
[Elixir Ecto Association - Oiita](https://qiita.com/sand/items/5581497972473e308f05)



# １．Generator　- phx.gen.html

　プロジェクトを作成し、DBを初期化します。

```
mix phx.new people
cd people
mix ecto.setup
```

　generatorのコマンド[phx.gen.html](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.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の複数形|

以下自動生成されたファイルに日本語でコメントを加えていきます。

# ２．DB関連モジュール

## ２－１．context module

　ドキュメントでは、**context** はresourceのAPI boundaryを提供するElixir moduleです、と述べられています。つまり **context** は、schemas や database に対して複数の操作が必要な時に、それらを一つの関数にまとめて、シンプルなインターフェースを提供してくれるmoduleです。**ラッパー関数の集まりです。** controllerからdatabaseにアクセスするときなどは、関数を一発呼ぶだけで、複雑な操作はこのmoduleで行ってくれます。

　~~（注意）users.exは自動生成されたものですが、長くなるのでコメントはカットしてあります。~~　<=コメントもそのまま掲載してます。

```elixir: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
```





## ２－２．schema module

　**Schema は DB テーブルを Elixir struct に map するために使われます。** テーブルの定義をstructに反映したものになります。**changeset** の定義も行われます。schemaとchangesetについては以下の過去記事を参照してください。

[Elixir Ecto チュートリアル - Qiita](https://qiita.com/sand/items/0861ad7a16f7ac612f41)
[Elixir Ecto のまとめ - Qiita](https://qiita.com/sand/items/71d0b35d74a4781f3564)



```elixir: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
```


## ２－３．schema table

　**schema table** の定義ファイルです。

```elixir: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
```




# ３．routerモジュール

　**Router** はクライアントからのリクエストを、パスを基に、適切なController（の関数）に振り分ける役割を果たします。

　phx.gen.htmlコマンドで、router.exに以下の一行を追加するようガイドされます。手動で追加します。

```elixir: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](https://hexdocs.pm/phoenix/routing.html)

```
---
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
---
```



# ４．controllerモジュールとViewモジュール

　phx.gen.htmlは、**Controller** と **View** のElixir moduleも自動生成します。


## ４－１．Controller

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

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



```elixir: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)](https://hexdocs.pm/phoenix/Phoenix.Controller.html#render/3) 
assignsを使ってtemplateを描画します。

render関数の使用例です。
```elixir:
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)](https://hexdocs.pm/phoenix/Phoenix.Controller.html#redirect/2)
与えられたURLへのredirect responseを送ります。
```elixir:
redirect(conn, to: "/login")

redirect(conn, external: "https://elixir-lang.org")
```

#### (3) Flash messages - [put_flash(conn, key, message)](https://hexdocs.pm/phoenix/Phoenix.Controller.html#put_flash/3)
conn に　Flash messages を保存します。
```elixir:
iex> conn = put_flash(conn, :info, "Welcome Back!")
iex >get_flash(conn, :info)
"Welcome Back!"
```

ちなみにuser_controller.exで保存されたFlash messagesはapp.html.heexの中で以下のように表示されています。

```elixir: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>
```

## ４－２．View

```elixir:lib/people_web/views/user_view.ex
defmodule PeopleWeb.UserView do
  use PeopleWeb, :view
end
```


# ５．templatesファイル

phx.gen.htmlは以下のtemplatesファイルも自動生成します。

Phoenixの基本的なことですが、templateがdataを受け取る方法は、conn の **assigns** mapを通してです。
 **"assigns"** storageはplugs 間同士で値を共有するのに使われます。

* [**assign(conn, key, value)**](https://hexdocs.pm/plug/1.13.6/Plug.Conn.html#assign/3) でconn のassignsに追加します。明示的に使われることはあまりないようですが。


assigns storage はmapです。
```elixir:
iex> conn.assigns[:hello]
nil
iex> conn = assign(conn, :hello, :world)
iex> conn.assigns[:hello]
:world
```


templateの中でassignsを参照するには　**@** ショートカットが用意されています。
```elixir:
render(conn, "show.html", username: "joe")
```

これはtemplateでは、以下のように参照されます。
```elixir:
<p>user: <%= @username %></p>
```


## ５－１．index

トップページです。user一覧を表示します。(4-1)のusersがこのtemplateで@usersで参照されています。

```html: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](https://hexdocs.pm/phoenix/routing.html)

ここでちょっと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"
```



## ５－２．show

　個別のuserを表示します。

```html: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>

```



## ５－３．new

　user作成画面を表示します。

　assignsには(4-2) (4-3)のchangesetが含まれている。assignsにaction:user_path(@conn, :create) を追加して、form.htmlをrenderします。



```html: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"に渡されます。

```elixir:
  def create(conn, %{"user" => user_params}) do
```

　ちなみに**assigns**にactionを追加するには**Map.put**を使っています。

```elixir:
Map.put(map, key, value)
Puts the given value under key in map
```



## ５－４．edit

　user編集画面を表示します。

　assignsには (4-5) (4-6) のchangesetが含まれている。assignsにaction:user_path(@conn, :update, @user) を追加して、form.htmlをrenderします。


```html: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"に渡されます。


```elixir:
def update(conn, %{"id" => id, "user" => user_params}) do
```





## ５－５．form

newとeditの中身です。両者では渡されてくるactionの値だけが違います。
changesetがRepo.[insert,update]で作成された場合は、「Oops,...」の警告文を出します。

```html: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 ドキュメント](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html)


**(A) form_for(form_data, action, options \\ [], fun)**
* form builder と 無名関数からformタグを生成する関数

```elixir:
<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
  Name: <%= text_input f, :name %>
<% end %>
```


**(B) Phoenix.LiveView integration**

しかしPhoenix.LiveViewとの統合でform_forは使わないよう案内されています。

```elixir:
<%= 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**

```elixir:
# 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**

```elixir:
# 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が返されます。

```elixir:
#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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/dd8ea7be-2350-af6f-27ac-74d4f41e28f4.png)




#### (4) new 画面に渡されるchangeset
changeset = **Users.change_user(%User{})** で作られたchangesetが, new 画面に渡されますが、以下のような内容になります。

```elixir:
#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」](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-a-note-on-errors)を参照してください。



今回は以上です。



**■ Elixir/Phoenixの基礎についてまとめた過去記事**
[Elixir Ecto チュートリアル - Qiita](https://qiita.com/sand/items/0861ad7a16f7ac612f41)
[Elixir Ecto のまとめ - Qiita](https://qiita.com/sand/items/71d0b35d74a4781f3564)
[Elixir Ecto Association - Qiita](https://qiita.com/sand/items/5581497972473e308f05)
[Phoenix1.6の基本的な仕組み - Qiita](https://qiita.com/sand/items/f42dc35c47589eab687e)
[Phoenixのログイン管理のためのSessionの使い方 - Qiita](https://qiita.com/sand/items/a88f1c39f3a6f7a01f99)
[Phoenix1.3のUserアカウントとSession - Qiita](https://qiita.com/sand/items/8e541f8a950ae6c63931)
[Phoenix1.3+Guardian1.0でJWT - Qiita](https://qiita.com/sand/items/393827194e687efafd4f)