[005.2] Getting Started with Phoenix

Creating a new app and generating some resources.

Subscribe now

Getting Started with Phoenix [04.29.2017]

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.

Project

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 Users. 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.

Summary

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!

Resources