私
- 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のスクリーンショットです)
schema_migrations て、完全に Rails と同じですねw
ふむふむ。 id が primary キーになってて。何一つ驚くところはありませんね。良い意味で「普通」。
schema_migrations の中身も Rails と同じ。
動作確認
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 詳細
[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 一覧
[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 と同じですね。
開発者コンソール
ちょっと開発者コンソール見てみましょう。
概ね想定内ですが、 websocket というのが繋がっているのと、frame という名前で live-reload 関連の(?)
Javascript を読み込んでいるところが目を引きますね。
エラー画面
スタックトレースと、それぞれのフレームのソースコードが見られます。
軽くソースコード探訪
route はさっき見たので...
migration
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みたいなやつ)
なんじゃないかと思います。
モデル
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 というのがよくわからないですね。
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 がメタプログラミングで動的に生成してくれる
処理になりますかねぇ?
コントローラー
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 インターフェースをくっつけているのだ、というポリシーを感じます。
ビュー
<%= 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 と違う部分も垣間見え、よくわかりませんでした(今後調べます)。