search
LoginSignup
7

More than 3 years have passed since last update.

posted at

updated at

Organization

【Phoenix1.4】(後編)パスワード認証&リレーションあり「ブログチュートリアル」

|> いいよね!Elixir!

(この記事は Elixir Advent Calendar 2018 6日目です。前回はkikuyutaさんのElixir を使うようになった経緯 〜電力システム制御の現場から〜です!)

前編は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の2日目に掲載しています
|> 【Phoenix1.4】(前編)パスワード認証&リレーションあり「ブログチュートリアル」

それでは後半をお楽しみ下さい!

|> 自分自身のuserリソースのみ変更できるように修正

|> userリソース

さて前編までで、基本的なログイン機能はできたのですが、今のままだと他の人のユーザアカウントをeditしたりdeleteすることができてしまいます
userコントローラのshowやindexメソッドは許可してedit, update, deleteメソッドを許可しないような実装にしてみましょう

現状こうなっていますが

lib/ex_blog_web/controllers/user_controller.ex
defmodule ExBlogWeb.UserController do
  use ExBlogWeb, :controller

  alias ExBlog.Accounts
  alias ExBlog.Accounts.User

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, "index.html", users: users)
  end

  def new(conn, _params) do
    changeset = Accounts.change_user(%User{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
      {:ok, _user} ->
        conn
        |> put_flash(:info, "User created successfully.")
        |> redirect(to: Routes.page_path(conn, :index))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    user = Accounts.get_user!(id)
    render(conn, "show.html", user: user)
  end

  def edit(conn, %{"id" => id}) do
    user = Accounts.get_user!(id)
    changeset = Accounts.change_user(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  end

  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Accounts.get_user!(id)

    case Accounts.update_user(user, user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User updated successfully.")
        |> redirect(to: Routes.user_path(conn, :show, user))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "edit.html", user: user, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    user = Accounts.get_user!(id)
    {:ok, _user} = Accounts.delete_user(user)

    conn
    |> put_flash(:info, "User deleted successfully.")
    |> redirect(to: Routes.user_path(conn, :index))
  end
end

以下のように変更します
プライベートメソッドであるis_authorizedを定義して、アクセスしたuserリソースのidとログイン中のuserのidが一致するかを確かめます
edit, update, deleteメソッドの実行前にis_authorizedを実行するPlugを定義します(Railsでいうとbefore_actionみたいなの)
各edit, update, deleteの実行はis_authrizedの後、つまりcurrent_userに対する操作だけすればいいのでリクエストのidは気にしなくてよくなります
あと、ついでにuser登録後についでにログインするようにしておきます

lib/ex_blog_web/controllers/user_controller.ex
defmodule ExBlogWeb.UserController do
  use ExBlogWeb, :controller

  alias ExBlog.Accounts
  alias ExBlog.Accounts.User
+  alias ExBlog.Accounts.Guardian
+
+  plug :is_authorized when action in [:edit, :update, :delete]

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, "index.html", users: users)
  end

  def new(conn, _params) do
    changeset = Accounts.change_user(%User{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
-      {:ok, _user} ->
+      {:ok, user} ->
        conn
        |> put_flash(:info, "User created successfully.")
-        |> redirect(to: Routes.page_path(conn, :index))
+        |> Guardian.Plug.sign_in(user)
+        |> redirect(to: Routes.user_path(conn, :show, user))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    user = Accounts.get_user!(id)
    render(conn, "show.html", user: user)
  end

-  def edit(conn, %{"id" => id}) do
-    user = Accounts.get_user!(id)
-    changeset = Accounts.change_user(user)
-    render(conn, "edit.html", user: user, changeset: changeset)
+  def edit(conn, _) do
+    changeset = Accounts.change_user(conn.assigns.current_user)
+    render(conn, "edit.html", user: conn.assigns.current_user, changeset: changeset)
  end

-  def update(conn, %{"id" => id, "user" => user_params}) do
-    user = Accounts.get_user!(id)
+  def update(conn, %{"user" => user_params}) do
+    user = conn.assigns.current_user

    case Accounts.update_user(user, user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User updated successfully.")
        |> redirect(to: Routes.user_path(conn, :show, user))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "edit.html", user: user, changeset: changeset)
    end
  end

-  def delete(conn, %{"id" => id}) do
-    user = Accounts.get_user!(id)
-    {:ok, _user} = Accounts.delete_user(user)
+  def delete(conn, _) do
+    {:ok, _user} = Accounts.delete_user(conn.assigns.current_user)

    conn
+    |> Guardian.Plug.sign_out()
    |> put_flash(:info, "User deleted successfully.")
-    |> redirect(to: Routes.user_path(conn, :index))
+    |> redirect(to: Routes.page_path(conn, :index))
  end

+  defp is_authorized(conn, _) do
+    current_user = Accounts.current_user(conn)
+     if current_user.id == String.to_integer(conn.params["id"]) do
+      assign(conn, :current_user, current_user)
+    else
+      conn
+      |> put_flash(:error, "You can't modify that page")
+      |> redirect(to: Routes.page_path(conn, :index))
+      |> halt()
+    end
+  end
end

accountsに現在のユーザを返すメソッドを追加します

lib/ex_blog/accounts/accounts.ex
+  def current_user(conn) do
+    Guardian.Plug.current_resource(conn)
+  end

これで自分以外のuserリソースをeditやupdateやdeleteしようとすると以下のようにフラッシュが出ます

image.png

articleリソースも同様に権限管理できますが、ログインを実装しarticleのcreateにuserリソースを関連付けることができるようになったので、まずはリレーションから進めていきたいと思います

|> リレーションを使って特定のuserが持つarticleの一覧を表示させる

さて、まずはuserが属するAccountsコンテキストとarticleが属するBlogコンテキストを変更します
articleやuserを取得するときにRepo.preload()をパイプさせることで、リレーション先のテーブルを取得できます
また、create_articleメソッドではuserを引数に取り、そのidをuser_idカラムに追加する処理を追加します

lib/ex_blog/accounts/accounts.ex
defmodule ExBlog.Accounts do
  @moduledoc """
  The Accounts context.
  """

  import Ecto.Query, warn: false
  alias ExBlog.Repo

  alias ExBlog.Accounts.User
  alias Comeonin.Bcrypt
+  alias ExBlog.Accounts.Guardian

  @doc """
  Returns the list of users.

  ## Examples

      iex> list_users()
      [%User{}, ...]

  """
  def list_users do
-    Repo.all(User)
+    User
+    |> Repo.all()
+    |> Repo.preload(:articles)
  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)
+  def get_user!(id) do
+    User
+    |> Repo.get!(id)
+    |> Repo.preload(:articles)
+  end


  @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{source: %User{}}

  """
  def change_user(%User{} = user) do
    User.changeset(user, %{})
  end

  def authenticate_user(email, plain_text_password) do
    query = from u in User, where: u.email == ^email
    Repo.one(query)
    |> check_password(plain_text_password)
  end
  defp check_password(nil, _), do: {:error, "Incorrect username or password"}
  defp check_password(user, plain_text_password) do
    case Bcrypt.checkpw(plain_text_password, user.password) do
      true -> {:ok, user}
      false -> {:error, "Incorrect username or password"}
    end
  end

  def current_user(conn) do
    Guardian.Plug.current_resource(conn)
  end
end
lib/ex_blog/blog/blog.ex
defmodule ExBlog.Blog do
  @moduledoc """
  The Blog context.
  """

  import Ecto.Query, warn: false
  alias ExBlog.Repo

  alias ExBlog.Blog.Article
+  alias ExBlog.Accounts.User

  @doc """
  Returns the list of articles.

  ## Examples

      iex> list_articles()
      [%Article{}, ...]

  """
  def list_articles do
-    Repo.all(Article)
+    Article
+    |> Repo.all()
+    |> Repo.preload(:user)
  end

  @doc """
  Gets a single article.

  Raises `Ecto.NoResultsError` if the Article does not exist.

  ## Examples

      iex> get_article!(123)
      %Article{}

      iex> get_article!(456)
      ** (Ecto.NoResultsError)

  """
-  def get_article!(id), do: Repo.get!(Article, id)
+  def get_article!(id) do
+    Article
+    |> Repo.get!(id)
+    |> Repo.preload(:user)
+  end

  @doc """
  Creates a article.

  ## Examples

      iex> create_article(%{field: value})
      {:ok, %Article{}}

      iex> create_article(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
-  def create_article(attrs \\ %{}) do
+  def create_article(%User{} = user, attrs \\ %{}) do
    %Article{}
    |> Article.changeset(attrs)
+    |> Ecto.Changeset.put_change(:user_id, user.id)
    |> Repo.insert()
  end

  @doc """
  Updates a article.

  ## Examples

      iex> update_article(article, %{field: new_value})
      {:ok, %Article{}}

      iex> update_article(article, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_article(%Article{} = article, attrs) do
    article
    |> Article.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a Article.

  ## Examples

      iex> delete_article(article)
      {:ok, %Article{}}

      iex> delete_article(article)
      {:error, %Ecto.Changeset{}}

  """
  def delete_article(%Article{} = article) do
    Repo.delete(article)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking article changes.

  ## Examples

      iex> change_article(article)
      %Ecto.Changeset{source: %Article{}}

  """
  def change_article(%Article{} = article) do
    Article.changeset(article, %{})
  end
end

create_articleメソッドを変更したので呼び出し側のarticleコントローラも引数を変更します

lib/ex_blog_web/controllers/article_controller.ex
  def create(conn, %{"article" => article_params}) do
-    case Blog.create_article(article_params) do
+    case Blog.create_article(Guardian.Plug.current_resource(conn), article_params) do
      {:ok, article} ->
        conn
        |> put_flash(:info, "Article created successfully.")
        |> redirect(to: Routes.article_path(conn, :show, article))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

これでロジック部分でリレーションを反映させることができました
次はビューを変更しましょう

|> ビューの変更

|> layout

lib/ex_blog_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Hello ExBlog!</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>">
  </head>

  <!-- phoenix.cssが邪魔するので暫定的にbodyに直接style -->
  <body style="margin-top: 0px">
    <header class="header">
      <div class="navbar navbar-dark bg-dark shadow-sm">
        <div class="container d-flex justify-content-between">
          <nav role="navigation">
            <ul class="nav nav-pills pull-right">
              <%= if current_user(@conn) do %>
                <li>
                  <%= link to: Routes.user_path(@conn, :show, current_user(@conn)) do %>
                    <button type="button" class="btn btn-default btn-sm">
                      Name: <b><%= current_user(@conn).name %></b>
                    </button>
                  <% end %>
                </li>
                <li>
                  <%= link to: Routes.session_path(@conn, :delete), method: :delete do %>
                    <button type="button" class="btn btn-default btn-sm">
                      Logout
                    </button>
                  <% end %>
                </li>
              <% else %>
                <li>
                  <%= link to: Routes.session_path(@conn, :new) do %>
                    <button type="button" class="btn btn-default btn-sm">
                      Login
                    </button>
                  <% end %>
                </li>
              <% end %>
            </ul>
          </nav>
        </div>
      </div>
    </header>
    <div class= "container">
      <p class="alert alert-success" role="alert"><%= get_flash(@conn, :success) %></p>
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>

      <main role="main">
        <%= render @view_module, @view_template, assigns %>
      </main>

    </div> <!-- /container -->
    <script src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

※Phoenix1.4からデフォルトのcssがbootstrapからMilligramに変わりました
上記のコードはbootstrap準拠で書いているのでスタイル崩れちゃいます><
気になる人は適宜スタイル修正するかbootstrap入れるかしてみてください
ただし単純にprev/repo/static/app.cssを置き換えるだけだと、webpackが自動でデフォルトに書き直す処理が入るのでダメみたいですね
今回は見た目後回しでいいと思うので、とりあえず先にすすみましょう

|> page/index

urlに打ち込むのは面倒なので、ログイン/ユーザ登録などへのボタンを追加

lib/ex_blog_web/templates/page/index.html.eex
<div class="jumbotron">
  <h2>Simple blog by Phoenix</h2>
  <%= unless current_user(@conn) do %>
    <p class="lead">
      <%= link to: Routes.user_path(@conn, :new) do %>
        <button type="button" class="btn btn-primary">
          Sign in!
        </button>
      <% end %>
    </p>
  <% end %>
</div>
lib/ex_blog_web/views/page_view.ex
defmodule ExBlogWeb.PageView do
  use ExBlogWeb, :view
  alias ExBlog.Accounts
  def current_user(conn) do
    Accounts.current_user(conn)
  end
end

|> user

ログインしているuser自身のリソースのみEdit, Deleteリンクが出るようにしましょう

lib/ex_blog_web/templates/user/show.html.eex
<h1>Show User</h1>

<ul>

  <li>
    <strong>Name:</strong>
    <%= @user.name %>
  </li>

  <li>
    <strong>Email:</strong>
    <%= @user.email %>
  </li>

</ul>

<%= if @user.id == current_user(@conn).id do %>
  <span><%= link "Edit", to: Routes.user_path(@conn, :edit, @user) %></span>
<% end %>
<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>

<h2>Listing Articles</h2>
 <table class="table">
  <thead>
    <tr>
      <th>Title</th>
      <th>Content</th>
       <th></th>
    </tr>
  </thead>
  <tbody>
<%= for article <- @user.articles do %>
    <tr>
      <td><%= article.title %></td>
      <td><%= article.content %></td>
       <td class="text-right">
        <span><%= link "Show", to: Routes.article_path(@conn, :show, article), class: "btn btn-default btn-xs" %></span>
        <%= if @user.id == current_user(@conn).id do %>
          <span><%= link "Edit", to: Routes.article_path(@conn, :edit, article), class: "btn btn-default btn-xs" %></span>
          <span><%= link "Delete", to: Routes.article_path(@conn, :delete, article), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %></span>
        <% end %>
      </td>
    </tr>
<% end %>
  </tbody>
</table>
 <%= if @user.id == current_user(@conn).id do %>
  <span><%= link "New Article", to: Routes.article_path(@conn, :new) %></span>
<% end %>
lib/ex_blog_web/views/user_view.ex
defmodule ExBlogWeb.UserView do
  use ExBlogWeb, :view
  alias ExBlog.Accounts
  def current_user(conn) do
    Accounts.current_user(conn)
  end
end

|> article

リレーションを反映させたことで@article.user.nameのように対応するuserの情報を取得することができるようになりました
またリレーションをたどることで、articleの所有しているユーザが分かるので、userは自分自身が所有しているarticleのみを削除&編集ボタン表示するようにします

lib/ex_blog_web/templates/article/index.html.eex
<h2>Listing Articles</h2>

<table class="table">
  <thead>
    <tr>
      <th>User</th>
      <th>Title</th>
      <th>Content</th>

      <th></th>
    </tr>
  </thead>
  <tbody>
<%= for article <- @articles do %>
    <tr>
      <td><%= article.user.name %></td>
      <td><%= article.title %></td>
      <td><%= article.content %></td>

      <td class="text-right">
        <span><%= link "Show", to: Routes.article_path(@conn, :show, article), class: "btn btn-default btn-xs" %></span>
        <%= if article.user.id == current_user(@conn).id do %>
          <span><%= link "Edit", to: Routes.article_path(@conn, :edit, article), class: "btn btn-default btn-xs" %></span>
          <span><%= link "Delete", to: Routes.article_path(@conn, :delete, article), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %></span>
        <% end %>
      </td>
    </tr>
<% end %>
  </tbody>
</table>

<span><%= link "New Article", to: Routes.article_path(@conn, :new) %></span>
lib/ex_blog_web/templates/article/show.html.eex
<h2>Show Article</h2>

<ul>

  <li>
    <strong>User:</strong>
    <%= @article.user.name %>
  </li>

  <li>
    <strong>Title:</strong>
    <%= @article.title %>
  </li>

  <li>
    <strong>Content:</strong>
    <%= @article.content %>
  </li>

</ul>
<%= if @article.user.id == current_user(@conn).id do %>
  <span><%= link "Edit", to: Routes.article_path(@conn, :edit, @article) %></span>
<% end %>
<span><%= link "Back", to: Routes.article_path(@conn, :index) %></span>
lib/ex_blog_web/views/article_view.ex
defmodule ExBlogWeb.ArticleView do
  use ExBlogWeb, :view
  alias ExBlog.Accounts
  def current_user(conn) do
    Accounts.current_user(conn)
  end
end

ここまでで画面を見てみましょう
koyoユーザでログインしlocalhost:4000/articlesに行きます
各種リンクボタンがあり、またKOYO'S PAGEを見るとログインユーザ名を反映して動的に描画されていることが分かります

image.png

さてnew articleをクリックして適当にarticleを生成します
このフォームにはuser_idを入力する部分はありませんが、先ほど実装したようにログインユーザのidが自動で入るようになっています
image.png

submitしてもう一度戻ると以下のようにuser名が自動登録され、また自分が所有しているarticleのみEdit, Deleteリンクが生成されていますね
image.png

さて、今は画面側で表示していないだけなので、直接URL叩けば自分が所有していないarticleを操作できます
試してみましょう
image.png
urlからsampleくんのidは1っぽいですね
localhost:4000/articles/1/editとすればeditできそうなので、やってみたらできました
image.png
これはセキュリティ的に大変良くないので、コントローラ側で権限があるのかをチェックする必要がありますね
これは先ほどuserリソースでも同じことしているのでおさらいです

|> 自分自身が所有しているarticleのみ編集できるようにする

lib/ex_blog_web/controllers/article_controller.ex
defmodule ExBlogWeb.ArticleController do
  use ExBlogWeb, :controller

  alias ExBlog.Blog
  alias ExBlog.Blog.Article
  alias ExBlog.Accounts
  alias ExBlog.Accounts.Guardian

  plug :is_authorized when action in [:edit, :update, :delete]

  def index(conn, _params) do
    articles = Blog.list_articles()
    render(conn, "index.html", articles: articles)
  end

  def new(conn, _params) do
    changeset = Blog.change_article(%Article{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"article" => article_params}) do
    case Blog.create_article(Guardian.Plug.current_resource(conn), article_params) do
      {:ok, article} ->
        conn
        |> put_flash(:info, "Article created successfully.")
        |> redirect(to: Routes.article_path(conn, :show, article))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    article = Blog.get_article!(id)
    render(conn, "show.html", article: article)
  end

  def edit(conn, _) do
    article = conn.assigns.article
    changeset = Blog.change_article(article)
    render(conn, "edit.html", article: article, changeset: changeset)
  end

  def update(conn, %{"article" => article_params}) do
    article = conn.assigns.article

    case Blog.update_article(article, article_params) do
      {:ok, article} ->
        conn
        |> put_flash(:info, "Article updated successfully.")
        |> redirect(to: Routes.article_path(conn, :show, article))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "edit.html", article: article, changeset: changeset)
    end
  end

  def delete(conn, _) do
    article = conn.assigns.article
    {:ok, _article} = Blog.delete_article(article)

    conn
    |> put_flash(:info, "Article deleted successfully.")
    |> redirect(to: Routes.article_path(conn, :index))
  end

  # Current_user can access only own resources
  # Check current_user's id match article.user.id
  defp is_authorized(conn, _) do
    current_user = Accounts.current_user(conn)
    article = Blog.get_article!(conn.params["id"])
    if current_user.id == article.user.id do
      assign(conn, :article, article)
    else
      conn
      |> put_flash(:error, "You can't modify that page")
      |> redirect(to: Routes.page_path(conn, :index))
      |> halt()
    end
  end
end

さてもう一度localhost:4000/articles/1/editを試してみると
image.png
ちゃんと制限されましたね!

|> user/showに所有しているarticleの一覧を表示

これで最後です!
user/showのページに自分が所有しているarticleの一覧を出してみましょう
とはいえ簡単でuser/show.html.eexに以下を追加するだけです

lib/ex_blog_web/templates/user/show.html.eex

<h2>Listing Articles</h2>
 <table class="table">
  <thead>
    <tr>
      <th>Title</th>
      <th>Content</th>
       <th></th>
    </tr>
  </thead>
  <tbody>
<%= for article <- @user.articles do %>
    <tr>
      <td><%= article.title %></td>
      <td><%= article.content %></td>
       <td class="text-right">
        <span><%= link "Show", to: Routes.article_path(@conn, :show, article), class: "btn btn-default btn-xs" %></span>
        <%= if @user.id == current_user(@conn).id do %>
          <span><%= link "Edit", to: Routes.article_path(@conn, :edit, article), class: "btn btn-default btn-xs" %></span>
          <span><%= link "Delete", to: Routes.article_path(@conn, :delete, article), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %></span>
        <% end %>
      </td>
    </tr>
<% end %>
  </tbody>
</table>
 <%= if @user.id == current_user(@conn).id do %>
  <span><%= link "New Article", to: Routes.article_path(@conn, :new) %></span>
<% end %>

koyoユーザはhoge, hoge1, hoge2のarticleリソースを持っていて、sampleユーザは別のhogeのarticleリソースを持っていますね
image.png

KOYO's PAGEリンクをクリックしてuser/showに遷移すると
image.png

koyoユーザが作成したarticleリソースの一覧が表示されています!

・・・これで一通り完成しました!お疲れ様です!
次はこれ以降の学習のヒントを示したいと思います

|> 追加課題

|> 自分なりの機能を追加してみる

このチュートリアルで簡単なブログアプリができました
ここまでやれば後はPhoenix公式ドキュメントを(英語苦手な人はGoogle翻訳駆使しながら)読んだり、EctoやGurdianのドキュメント読めば大抵のことはできるのではないでしょうか!(例えばarticleに対するcommentリソースを実装してみるといいかもしれないですね)
ぜひ自分のアイデアを実現させてみてください

|> テスト書いてみる

実運用ではテストは必須です!(というより、今回は説明のためテスト書かずに実装しましたが、慣れてきたらテストを先に書くTDDでやった方がいいです)
ElixirにはExUnitというテストフレームワークが標準装備なので、ドキュメント読みながら実装してみましょう!
ちなみにmix phx.gen.htmlで生成したコードにはすでにテストが自動生成されているので、まずはそれを参考にしてみるといいと思います

|> Gigalixirにデプロイしてみる

なんと!Elixirには専門のPaasであるGigalixirが存在します
使い勝手はHerokuみたいな感じです
ちょっと設定すればデプロイできるので、piacereさんの以下の資料を読んでチャレンジしてみてください!

【Gigalixir編①】Elixir/Phoenix本番リリース: 初期PJリリースまで

|> 参考

Phoenix1.3バージョンですが、githubのコードとGigalixirにデプロイしたアプリを置いておきますね!(Gigalixirの方は無料だと1アプリまでなので、そのうち消します・・・リンク切れていたらごめんなさい><)
テストもちょっとだけ書いたので参考になるかもしれません

ちなみに1.3ではpage_path(...)と記述していたところが1.4ではRoutes.page_path(...)というようにRoutesというモジュール名を明示的に書くように変更されているので、バージョンをまたぐ場合は適宜読み替えてください

|>Github
|>ex-blog.gigalixirapp.com

|> まとめ

Elixirのコードは可読性高くまとまりますね!
Phoenixを使えばRailsと遜色ないスピードで開発できて、しかもRailsの悩みである性能問題をある程度解消できます
おまけにアクターモデル由来の軽量プロセスにより耐障害性も高いんです(エッヘン)

このように単一CPU性能の向上を待たずして、メニーコアの性能を生かし切る可能性を秘めたElixir(Phoenix)が新たなトレンドになる日も近いかもしれません
是非触ってみてください!

次回はhykwさんの記事です!

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
What you can do with signing up
7