LoginSignup
32
34

More than 5 years have passed since last update.

Phoenix入門:10分で作るブログ

Last updated at Posted at 2017-08-16

  • Rails を 10 年以上使い続けています。
  • Elixir 勉強中です。プロセスかわいいよ、プロセス。
  • MacOS sierra 10.12.6 です。

インストール

ここに沿う感じで。

$ brew upgrade elixir
$ elixir --version
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.5.1
$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
$ mix phoenix.new -v
Phoenix v1.3.0

プロジェクト作成

まずはプロジェクトを作ります。WAFのHello Worldといえば、10分で作るブログ作成ですよね。

$ mix phx.new blog                                                                                                          
* creating blog/config/config.exs
* creating blog/config/dev.exs
* creating blog/config/prod.exs
* creating blog/config/prod.secret.exs
* creating blog/config/test.exs
* creating blog/lib/blog/application.ex
* creating blog/lib/blog.ex
* creating blog/lib/blog_web/channels/user_socket.ex
* creating blog/lib/blog_web/views/error_helpers.ex
* creating blog/lib/blog_web/views/error_view.ex
* creating blog/lib/blog_web/endpoint.ex
* creating blog/lib/blog_web/router.ex
* creating blog/lib/blog_web.ex
* creating blog/mix.exs
* creating blog/README.md
* creating blog/test/support/channel_case.ex
* creating blog/test/support/conn_case.ex
* creating blog/test/test_helper.exs
* creating blog/test/blog_web/views/error_view_test.exs
* creating blog/lib/blog_web/gettext.ex
* creating blog/priv/gettext/en/LC_MESSAGES/errors.po
* creating blog/priv/gettext/errors.pot
* creating blog/lib/blog/repo.ex
* creating blog/priv/repo/seeds.exs
* creating blog/test/support/data_case.ex
* creating blog/lib/blog_web/controllers/page_controller.ex
* creating blog/lib/blog_web/templates/layout/app.html.eex
* creating blog/lib/blog_web/templates/page/index.html.eex
* creating blog/lib/blog_web/views/layout_view.ex
* creating blog/lib/blog_web/views/page_view.ex
* creating blog/test/blog_web/controllers/page_controller_test.exs
* creating blog/test/blog_web/views/layout_view_test.exs
* creating blog/test/blog_web/views/page_view_test.exs
* creating blog/.gitignore
* creating blog/assets/brunch-config.js
* creating blog/.gitignore                                                                                                                          
* creating blog/assets/brunch-config.js
* creating blog/assets/css/app.css
* creating blog/assets/css/phoenix.css
* creating blog/assets/js/app.js
* creating blog/assets/js/socket.js
* creating blog/assets/package.json
* creating blog/assets/static/robots.txt
* creating blog/assets/static/images/phoenix.png
* creating blog/assets/static/favicon.ico

Fetch and install dependencies? [Yn] y
* running mix deps.get
* running mix deps.compile
* running cd assets && npm install && node node_modules/brunch/bin/brunch build

We are all set! Go into your application by running:

    $ cd blog

Then configure your database in config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server

ふむふむ。問題ないですね。一応、gitに入れておきましょうか。

$ cd blog
$ git init
$ git add .
$ git commit -m "initial"

プロジェクト周辺探訪

ちょっと寄り道。

mix deps.tree ってすると、依存ライブラリの一覧などが確認できるらしいです。

$ mix deps.tree
blog
├── gettext ~> 0.11 (Hex package)
├── phoenix_pubsub ~> 1.0 (Hex package)
├── cowboy ~> 1.0 (Hex package)
│   ├── cowlib ~> 1.0.2 (Hex package)
│   └── ranch ~> 1.3.2 (Hex package)
├── phoenix_html ~> 2.10 (Hex package)
│   └── plug ~> 1.0 (Hex package)
│       ├── cowboy ~> 1.0.1 or ~> 1.1 (Hex package)
│       └── mime ~> 1.0 (Hex package)
├── phoenix ~> 1.3.0 (Hex package)
│   ├── cowboy ~> 1.0 (Hex package)
│   ├── phoenix_pubsub ~> 1.0 (Hex package)
│   ├── plug ~> 1.3.3 or ~> 1.4 (Hex package)
│   └── poison ~> 2.2 or ~> 3.0 (Hex package)
├── phoenix_live_reload ~> 1.0 (Hex package)
│   ├── file_system ~> 0.1 (Hex package)
│   └── phoenix ~> 1.0 or ~> 1.2 or ~> 1.3 (Hex package)
├── postgrex >= 0.0.0 (Hex package)
│   ├── connection ~> 1.0 (Hex package)
│   ├── db_connection ~> 1.1 (Hex package)
│   │   ├── connection ~> 1.0.2 (Hex package)
│   │   └── poolboy ~> 1.5 (Hex package)
│   └── decimal ~> 1.0 (Hex package)
└── phoenix_ecto ~> 3.2 (Hex package)
    ├── ecto ~> 2.1 (Hex package)
    │   ├── db_connection ~> 1.1 (Hex package)
    │   ├── decimal ~> 1.2 (Hex package)
    │   ├── poison ~> 2.2 or ~> 3.0 (Hex package)
    │   ├── poolboy ~> 1.5 (Hex package)
    │   └── postgrex ~> 0.13.0 (Hex package)
    ├── phoenix_html ~> 2.9 (Hex package)
    └── plug ~> 1.0 (Hex package)

あと、 Rails と違って、これらの依存ライブラリは全部プロジェクト内(blog/deps以下)に
存在するみたいです(Railsでいうと gem を vendor 以下に入れたような状態)。

そのほか、 mix help などを見ると(Rails でいうと、 rake -T みたいなやつ)、
何ができるかだいたいわかります。

$ mix help
mix                    # Runs the default task (current: "mix run")
mix app.start          # Starts all registered apps
mix app.tree           # Prints the application tree
mix archive            # Lists installed archives
mix archive.build      # Archives this project into a .ez file
mix archive.install    # Installs an archive locally
mix archive.uninstall  # Uninstalls archives
mix clean              # Deletes generated application files
mix cmd                # Executes the given command
mix compile            # Compiles source files
mix deps               # Lists dependencies and their status
mix deps.clean         # Deletes the given dependencies' files
mix deps.compile       # Compiles dependencies
mix deps.get           # Gets all out of date dependencies
mix deps.tree          # Prints the dependency tree
mix deps.unlock        # Unlocks the given dependencies
mix deps.update        # Updates the given dependencies
mix do                 # Executes the tasks separated by comma
mix ecto               # Prints Ecto help information
mix ecto.create        # Creates the repository storage
mix ecto.drop          # Drops the repository storage
mix ecto.dump          # Dumps the repository database structure
mix ecto.gen.migration # Generates a new migration for the repo
mix ecto.gen.repo      # Generates a new repository
mix ecto.load          # Loads previously dumped database structure
mix ecto.migrate       # Runs the repository migrations
mix ecto.migrations    # Displays the repository migration status
mix ecto.rollback      # Rolls back the repository migrations
mix escript            # Lists installed escripts
mix escript.build      # Builds an escript for the project
mix escript.install    # Installs an escript locally
mix escript.uninstall  # Uninstalls escripts
mix gettext.extract    # Extracts translations from source code
mix gettext.merge      # Merge template files into translation files
mix help               # Prints help information for tasks
mix hex                # Prints Hex help information
mix hex.audit          # Shows retired Hex dependencies
mix hex.build          # Builds a new package version locally
mix hex.config         # Reads, updates or deletes Hex config
mix hex.docs           # Fetches or opens documentation of a package
mix hex.info           # Prints Hex information
mix hex.outdated       # Shows outdated Hex deps for the current project
mix hex.owner          # Manages Hex package ownership
mix hex.publish        # Publishes a new package version
mix hex.repo           # Manages Hex repositories
mix hex.retire         # Retires a package version
mix hex.search         # Searches for package names
mix hex.user           # Manages your Hex user account
mix loadconfig         # Loads and persists the given configuration
mix local              # Lists local tasks
mix local.hex          # Installs Hex locally
mix local.phoenix      # Updates Phoenix locally
mix local.phx          # Updates the Phoenix project generator locally
mix local.public_keys  # Manages public keys
mix local.rebar        # Installs Rebar locally
mix new                # Creates a new Elixir project
mix phoenix.gen.html   # Generates controller, model and views for an HTML based resource
mix phoenix.new        # Creates a new Phoenix v1.3.0 application
mix phoenix.server     # Starts applications and their servers
mix phx.digest         # Digests and compresses static files
mix phx.digest.clean   # Removes old versions of static assets.
mix phx.gen.channel    # Generates a Phoenix channel
mix phx.gen.context    # Generates a context with functions around an Ecto schema
mix phx.gen.embedded   # Generates an embedded Ecto schema file
mix phx.gen.html       # Generates controller, views, and context for an HTML resource
mix phx.gen.json       # Generates controller, views, and context for a JSON resource
mix phx.gen.presence   # Generates a Presence tracker
mix phx.gen.schema     # Generates an Ecto schema and migration file
mix phx.gen.secret     # Generates a secret
mix phx.new            # Creates a new Phoenix v1.3.0 application
mix phx.new.ecto       # Creates a new Ecto project within an umbrella project
mix phx.new.web        # Creates a new Phoenix web project within an umbrella project
mix phx.routes         # Prints all routes
mix phx.server         # Starts applications and their servers
mix profile.cprof      # Profiles the given file or expression with cprof
mix profile.fprof      # Profiles the given file or expression with fprof
mix run                # Runs the given file or expression
mix test               # Runs a project's tests
mix xref               # Performs cross reference checks
iex -S mix             # Starts IEx and runs the default task

DBの作成

上記で ecto.create しろって出てたので、やります。

$ mix ecto.create

DB を見ると、blog_dev という名前の空のDBができてますね。

Scaffold 的なやつ

phx.gen.html というのでやるらしいです。

$ mix phx.gen.html Articles Article articles title body:text
* creating lib/blog_web/controllers/article_controller.ex
* creating lib/blog_web/templates/article/edit.html.eex
* creating lib/blog_web/templates/article/form.html.eex
* creating lib/blog_web/templates/article/index.html.eex
* creating lib/blog_web/templates/article/new.html.eex
* creating lib/blog_web/templates/article/show.html.eex
* creating lib/blog_web/views/article_view.ex
* creating test/blog_web/controllers/article_controller_test.exs
* creating lib/blog/articles/article.ex
* creating priv/repo/migrations/20170816110139_create_articles.exs
* creating lib/blog/articles/articles.ex
* injecting lib/blog/articles/articles.ex
* creating test/blog/articles/articles_test.exs
* injecting test/blog/articles/articles_test.exs

Add the resource to your browser scope in lib/blog_web/router.ex:

    resources "/articles", ArticleController


Remember to update your repository by running migrations:

    $ mix ecto.migrate


git commit しておきましょう。私は spacemacs + magit でやるので、
コマンドラインでどうやるのが適切なのかは知らず、ここには書けませんが、
みなさんいい感じに git add して git commit してください。

Rails 同様、型名を省略すると string 型として扱ってくれるようです。

Rails との違いとしては

  • コンテキスト名
  • リソース名単数
  • リソース名複数(テーブル名?)

というように同じような名前を3つ書かないといけない点ですね。

また、Rails だと routes への追加も自動でやってくれますが、
phoenix はそうではないみたいです。

ちなみに、この状態で http://localhost:4000/ にアクセスすると、
ターミナルにこんなエラーが出てます。

== Compilation error in file lib/blog_web/views/article_view.ex ==
** (CompileError) lib/blog_web/templates/article/edit.html.eex:3: undefined function article_path/3
    (stdlib) lists.erl:1338: :lists.foreach/2
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
    (elixir) lib/kernel/parallel_compiler.ex:121: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/1

なるほど、 article_path が定義されてない、と。完全に route のせいですね。

route の追加

defmodule BlogWeb.Router do
  use BlogWeb, :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 "/", BlogWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index

    resources "/articles", ArticleController # ここでいいのかな?
  end

  # Other scopes may use custom stacks.
  # scope "/api", BlogWeb do
  #   pipe_through :api
  # end
end

これで再び http://localhost:4000/ へアクセスすると、問題なく表示されます。

ターミナルの方にはもうエラーは出なくなり、

[info] GET /
[debug] Processing with BlogWeb.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 271µs

って出てます。

ここでも git commit しておきましょう。

migrate

もう一つ、 migrate しろって出てましたね。

$ mix ecto.migrate

20:25:30.733 [info]  == Running Blog.Repo.Migrations.CreateArticles.change/0 forward

20:25:30.733 [info]  create table articles

20:25:30.746 [info]  == Migrated in 0.0s

ふむふむ。 DB を見ると...(Postico.appのスクリーンショットです)

migrateでできたテーブル群

schema_migrations て、完全に Rails と同じですねw

articlesテーブルの構造

ふむふむ。 id が primary キーになってて。何一つ驚くところはありませんね。良い意味で「普通」。

schema_migrations内のデータ

schema_migrations の中身も Rails と同じ。

動作確認

Article 一覧

Article一覧

おー!出ましたねぇ!データの登録をしてみましょう。

Article 登録

Article登録

[info] POST /articles
[debug] Processing with BlogWeb.ArticleController.create/2
  Parameters: %{"_csrf_token" => "RjolInAZHU0VGxZHPlY7MQI/YFQqEAAA0kRq4aO8PZX1OburoUY9KQ==", "_utf8" => "✓", "article" => %{"body" => "こんにちは。ブログ始め
ました。\r\n\r\nではでは。", "title" => "ブログはじめました"}}
  Pipelines: [:browser]
[debug] QUERY OK db=9.2ms
INSERT INTO "articles" ("body","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["こんにちは。ブログ始めました。\r\n\r\nではでは。", "
ブログはじめました", {{2017, 8, 16}, {11, 32, 14, 597018}}, {{2017, 8, 16}, {11, 32, 14, 660880}}]
[info] Sent 302 in 113ms

Rails に比べると、若干 SQL が見にくいかな。

Article 詳細

Article詳細

[info] GET /articles/1
[debug] Processing with BlogWeb.ArticleController.show/2
  Parameters: %{"id" => "1"}
  Pipelines: [:browser]
[debug] QUERY OK source="articles" db=8.5ms queue=0.4ms
SELECT a0."id", a0."body", a0."title", a0."inserted_at", a0."updated_at" FROM "articles" AS a0 WHERE (a0."id" = $1) [1]
[info] Sent 200 in 10ms

再度、Aritlce一覧に戻って見ましょう。

データが入った状態の Article 一覧

再度、Aritlce一覧

[info] GET /articles
[debug] Processing with BlogWeb.ArticleController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[debug] QUERY OK source="articles" db=1.7ms
SELECT a0."id", a0."body", a0."title", a0."inserted_at", a0."updated_at" FROM "articles" AS a0 []
[info] Sent 200 in 2ms

ふむふむ。デザインはスッキリしてますが、付いている機能は概ね Rails と同じですね。

開発者コンソール

ちょっと開発者コンソール見てみましょう。

開発者コンソールの Network

概ね想定内ですが、 websocket というのが繋がっているのと、frame という名前で live-reload 関連の(?)
Javascript を読み込んでいるところが目を引きますね。

エラー画面

エラー画面

スタックトレースと、それぞれのフレームのソースコードが見られます。

軽くソースコード探訪

route はさっき見たので...

migration

/blog/priv/repo/migrations/20170816110139_create_articles.exs
defmodule Blog.Repo.Migrations.CreateArticles do
  use Ecto.Migration

  def change do
    create table(:articles) do
      add :title, :string
      add :body, :text

      timestamps()
    end

  end
end

ふむ。ここも驚くところは全くありませんね。
priv ってパスに入れてるところに、やや工夫が見られます。
repo ってのは DB アクセスモジュールの名前(ActiveRecordみたいなやつ)
なんじゃないかと思います。

モデル

/blog/lib/blog/articles/article.ex
defmodule Blog.Articles.Article do
  use Ecto.Schema
  import Ecto.Changeset
  alias Blog.Articles.Article


  schema "articles" do
    field :body, :string
    field :title, :string

    timestamps()
  end

  @doc false
  def changeset(%Article{} = article, attrs) do
    article
    |> cast(attrs, [:title, :body])
    |> validate_required([:title, :body])
  end
end

ここにも DB の定義があるのがやや驚きなのと、
changeset というのがよくわからないですね。

/blog/lib/blog/articles/articles.ex
defmodule Blog.Articles do
  @moduledoc """
  The Articles context.
  """

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

  alias Blog.Articles.Article

  @doc """
  Returns the list of articles.

  ## Examples

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

  """
  def list_articles do
    Repo.all(Article)
  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)

  @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
    %Article{}
    |> Article.changeset(attrs)
    |> 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

ほほーぅ。これは Rails だと ActiveRecord がメタプログラミングで動的に生成してくれる
処理になりますかねぇ?

コントローラー

/blog/lib/blog_web/controllers/article_controller.ex
defmodule BlogWeb.ArticleController do
  use BlogWeb, :controller

  alias Blog.Articles
  alias Blog.Articles.Article

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

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

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

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

  def edit(conn, %{"id" => id}) do
    article = Articles.get_article!(id)
    changeset = Articles.change_article(article)
    render(conn, "edit.html", article: article, changeset: changeset)
  end

  def update(conn, %{"id" => id, "article" => article_params}) do
    article = Articles.get_article!(id)

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

  def delete(conn, %{"id" => id}) do
    article = Articles.get_article!(id)
    {:ok, _article} = Articles.delete_article(article)

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

だいたい想定内のコードですが、 相変わらず changeset というのがよくわからないのと、
conn というのもわからないですね。

それから、モデルは lib/blog というパスの下にあるのに対し、
コントローラーとビューは lib/blog_webというパスにあるのが目を引きます。
ビジネスロジックに Web インターフェースをくっつけているのだ、というポリシーを感じます。

ビュー

/blog/lib/blog_web/templates/article/form.html.eex
<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <div class="form-group">
    <%= label f, :title, class: "control-label" %>
    <%= text_input f, :title, class: "form-control" %>
    <%= error_tag f, :title %>
  </div>

  <div class="form-group">
    <%= label f, :body, class: "control-label" %>
    <%= textarea f, :body, class: "form-control" %>
    <%= error_tag f, :body %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

すごく...Railsです...

error_tag ってのが Rails とは違うのと、ここにも出てきた changeset。よくわかりません。

まとめ

  • Phoenix で Scaffold がほとんどつまずくことなくできて、出来上がったアプリも全く問題なく動作しました。
  • コードをほぼ書きませんでした。routerを手で追記した1行ぐらい?コードとは呼べない。
  • 非常に Rails に似ている部分が多いことがわかりました。
  • それと同時に、 Rails と違う部分も垣間見え、よくわかりませんでした(今後調べます)。
32
34
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
32
34