This week we're focusing on getting comfortable with Phoenix quickly, by starting to build out the Firestorm Forum's views. Today we'll start a new app and generate a few resources for it. Let's get started.
We're starting a new project for this, so that we learn Phoenix in its most
basic form as a starting point. Let's create a new Phoenix application,
firestorm_web
mix phx.new firestorm_web
This generates the new application, then installs the Elixir and JavaScript
dependencies for it. Now we can cd
into the directory, create our database,
and run the app:
cd firestorm_web
mix ecto.create
mix phx.server
Now your phoenix application is up and running. You can visit it at http://localhost:4000. It has a route and a test out of the gate, so let's run the tests:
mix test
We can look at the PageControllerTest
that was generated:
vim test/web/controllers/page_controller_test.exs
defmodule FirestormWeb.Web.PageControllerTest do
use FirestormWeb.Web.ConnCase
test "GET /", %{conn: conn} do
conn = get conn, "/"
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
end
end
Here we see how to get
a given path and assert that its response is an HTTP
200 OK code containing a body with the text Welcome to Phoenix!
somewhere
inside of it. Let's look at the controller that serves this request:
vim lib/firestorm_web/web/controllers/page_controller.ex
defmodule FirestormWeb.Web.PageController do
use FirestormWeb.Web, :controller
def index(conn, _params) do
render conn, "index.html"
end
end
This is rendering the template at
lib/firestorm_web/web/templates/page/index.html.eex
. How did Phoenix know to
route this request to that controller? Let's look at the router.
vim lib/firestorm_web/web/router.ex
defmodule FirestormWeb.Web.Router do
use FirestormWeb.Web, :router
# In Phoenix you set up pipelines for various plugs.
# Plugs take a `Plug.Conn` and modify it - this is the default browser stack
# that Phoenix installs.
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
# Here's a separate default pipeline for API endpoints
pipeline :api do
plug :accepts, ["json"]
end
# When a request comes through for the root, this scope is applied.
# The second argument it the default namespace that we will use internally -
# this way we can avoid prefixing everything with FirestormWeb.Web
scope "/", FirestormWeb.Web do
pipe_through :browser # Use the default browser stack
# When someone `GET`s the root path, we'll handle the request with the
# `PageController`'s `index` function.
get "/", PageController, :index
end
# Other scopes may use custom stacks.
# scope "/api", FirestormWeb.Web do
# pipe_through :api
# end
end
So this is a starter Phoenix app. Of course, we'd like to have some interaction with a database. We can generate a resource and its corresponding CRUD UI using phoenix generators:
mix help phx.gen.html
mix phx.gen.html
Generates controller, views, and context for an HTML resource.
mix phx.gen.html Accounts User users name:string age:integer
The first argument is the context module followed by the schema module and its
plural name (used as the schema table name).
The context is an Elixir module that serves as an API boundary for the given
resource. A context often holds many related resources. Therefore, if the
context already exists, it will be augmented with functions for the given
resource. Note a resource may also be split over distinct contexts (such as
Accounts.User and Payments.User).
The schema is responsible for mapping the database fields into an Elixir
struct.
Overall, this generator will add the following files to lib/your_app:
• a context module in accounts/accounts.ex, serving as the API boundary
• a schema in accounts/user.ex, with an accounts_users table
• a view in web/views/user_view.ex
• a controller in web/controllers/user_controller.ex
• default CRUD templates in web/templates/user
A migration file for the repository and test files for the context and
controller features will also be generated.
## table
By default, the table name for the migration and schema will be the plural name
provided for the resource. To customize this value, a --table option may be
provided. For example:
mix phx.gen.html Accounts User users --table cms_users
## binary_id
Generated migration can use binary_id for schema's primary key and its
references with option --binary-id.
## Default options
This generator uses default options provided in the :generators configuration
of your application. These are the defaults:
config :your_app, :generators,
migration: true,
binary_id: false,
sample_binary_id: "11111111-1111-1111-1111-111111111111"
You can override those options per invocation by providing corresponding
switches, e.g. --no-binary-id to use normal ids despite the default
configuration or --migration to force generation of the migration.
Read the documentation for phx.gen.schema for more information on attributes.
This tells us how to generate a new resource inside of a Context
. A Context
is the API boundary for interacting with the resource. Let's create a Forums
context with a User
resource backed by a forums_users
table containing
username
, name
, and email
fields:
mix phx.gen.html Forums User users username:string email:string name:string
This also generated some HTML CRUD views and controller for us, so let's do what it says and add the controller to our routes:
vim lib/firestorm_web/web/router.ex
defmodule FirestormWeb.Web.Router do
# ...
scope "/", FirestormWeb.Web do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
resources "/users", UserController
end
# ...
end
Now we can run the migrations and restart the server:
mix ecto.migrate
mix phx.server
From here, if we visit http://localhost:4000/users we can see we have the
means to create, retrieve, update, and delete User
s. This is what you expect
out of a generator. Let's have a look at the tests it generated.
vim test/forums_test.exs
This is the generated test for our Forums
context.
defmodule FirestormWeb.ForumsTest do
use FirestormWeb.DataCase
alias FirestormWeb.Forums
alias FirestormWeb.Forums.User
@create_attrs %{email: "some email", name: "some name", username: "some username"}
@update_attrs %{email: "some updated email", name: "some updated name", username: "some updated username"}
@invalid_attrs %{email: nil, name: nil, username: nil}
# Here's a function to return a user fixture. This takes the place of our
# factories from last week, for now.
def fixture(:user, attrs \\ @create_attrs) do
{:ok, user} = Forums.create_user(attrs)
user
end
# We can list all the users
test "list_users/1 returns all users" do
user = fixture(:user)
assert Forums.list_users() == [user]
end
# We can get a user by id
test "get_user! returns the user with given id" do
user = fixture(:user)
assert Forums.get_user!(user.id) == user
end
# We can create a user
test "create_user/1 with valid data creates a user" do
assert {:ok, %User{} = user} = Forums.create_user(@create_attrs)
assert user.email == "some email"
assert user.name == "some name"
assert user.username == "some username"
end
test "create_user/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Forums.create_user(@invalid_attrs)
end
# We can update a user
test "update_user/2 with valid data updates the user" do
user = fixture(:user)
assert {:ok, user} = Forums.update_user(user, @update_attrs)
assert %User{} = user
assert user.email == "some updated email"
assert user.name == "some updated name"
assert user.username == "some updated username"
end
test "update_user/2 with invalid data returns error changeset" do
user = fixture(:user)
assert {:error, %Ecto.Changeset{}} = Forums.update_user(user, @invalid_attrs)
assert user == Forums.get_user!(user.id)
end
# And we can delete a user
test "delete_user/1 deletes the user" do
user = fixture(:user)
assert {:ok, %User{}} = Forums.delete_user(user)
assert_raise Ecto.NoResultsError, fn -> Forums.get_user!(user.id) end
end
# We can also get a user changeset
test "change_user/1 returns a user changeset" do
user = fixture(:user)
assert %Ecto.Changeset{} = Forums.change_user(user)
end
end
Contexts are a new feature in Phoenix 1.3. They're nice because they move a lot of logic that previously might have been placed into the controller, into simpler functions. This makes it easier to test the logic of your application separately from the web pieces. Let's look at the context file:
defmodule FirestormWeb.Forums do
@moduledoc """
The boundary for the Forums system.
"""
import Ecto.{Query, Changeset}, warn: false
alias FirestormWeb.Repo
alias FirestormWeb.Forums.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{source: %User{}}
"""
def change_user(%User{} = user) do
user_changeset(user, %{})
end
defp user_changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:username, :email, :name])
|> validate_required([:username, :email, :name])
end
end
This is just a boundary that makes it easy for the rest of our application to
not talk in terms of the underlying data store, but rather in the terms of the
data models we care about. We shouldn't care that users are stored in a
PostgreSQL database via Ecto. We just want to get a User
! That's what contexts
allow. Let's see what the controller code looks like:
vim lib/firestorm_web/web/controllers/user_controller.ex
defmodule FirestormWeb.Web.UserController do
use FirestormWeb.Web, :controller
alias FirestormWeb.Forums
def index(conn, _params) do
users = Forums.list_users()
render(conn, "index.html", users: users)
end
def new(conn, _params) do
changeset = Forums.change_user(%FirestormWeb.Forums.User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
case Forums.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
def show(conn, %{"id" => id}) do
user = Forums.get_user!(id)
render(conn, "show.html", user: user)
end
def edit(conn, %{"id" => id}) do
user = Forums.get_user!(id)
changeset = Forums.change_user(user)
render(conn, "edit.html", user: user, changeset: changeset)
end
def update(conn, %{"id" => id, "user" => user_params}) do
user = Forums.get_user!(id)
case Forums.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
def delete(conn, %{"id" => id}) do
user = Forums.get_user!(id)
{:ok, _user} = Forums.delete_user(user)
conn
|> put_flash(:info, "User deleted successfully.")
|> redirect(to: user_path(conn, :index))
end
end
This is just a thin shim over the logic that makes up our app. Fantastic! To wrap up the intro to Phoenix, we should see where the templates live.
vim lib/firestorm_web/web/templates/user/index.html.eex
<h2>Listing Users</h2>
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for user <- @users do %>
<tr>
<td><%= user.username %></td>
<td><%= user.email %></td>
<td><%= user.name %></td>
<td class="text-right">
<span><%= link "Show", to: user_path(@conn, :show, user), class: "btn btn-default btn-xs" %></span>
<span><%= link "Edit", to: user_path(@conn, :edit, user), class: "btn btn-default btn-xs" %></span>
<span><%= link "Delete", to: user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %></span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span><%= link "New User", to: user_path(@conn, :new) %></span>
This is an eex
template. We generate normal HTML and intersperse the
<%= expression %>
tags to execute Elixir code inside the template.
That wraps up today's work. Today we generated a new Phoenix application with tests as well as a new resource, and looked at how it all fits together in a bit of detail. Tomorrow we'll look at the frontend a bit more. See you soon!