Edited at

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

More than 1 year has passed since last update.



  • Rails を 10 年以上使い続けています。

  • Elixir 勉強中です。プロセスかわいいよ、プロセス。

  • MacOS sierra 10.12.6 です。


インストール

ここに沿う感じで。

https://hexdocs.pm/phoenix/installation.html#content

$ 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 と違う部分も垣間見え、よくわかりませんでした(今後調べます)。