elixir-blog-tutorial icon indicating copy to clipboard operation
elixir-blog-tutorial copied to clipboard

Blog Tutorial

Please note that this was written for a much older version of Phoenix. While some of the concepts may apply, it is worth using this in tandem with the official docs.

Getting Phoenix

Since the blog application is built using the Phoenix framework, the first step is to generate a new Phoenix application.

  1. Get phoenix by running git clone [email protected]:phoenixframework/phoenix.git
  2. cd phoenix
  3. Get the dependencies and compile mix do deps.get compile
  4. Generate a new Phoenix application mix phoenix.new blog ../blog

This will create a Phoenix application with the default skeleton. This also needs to fetch the dependencies and compile, then we can view the default Phoenix home page in the browser.

  1. cd ../blog
  2. mix do deps.get, compile
  3. mix phoenix.server
  4. Navigate to http://localhost:4000

This is probably a good point to commit the application so that our code doesn't get mixed in with the code generated by Phoenix.

  1. git init
  2. git add .
  3. git commit

Adding a database

Since this is a blog, our posts and comments will need to sit in a database somewhere, in this case a PostgreSQL database. We're going to use Ecto to interact with the database. We also need to use Postgrex which is used by the Ecto PostgreSQL adapter.

To add dependencies to an Elixir application we use have to update the mix.exs file with our dependencies. Since the two packages we are using exist on the Hex package manager we can simply specify the name of the dependency and it will be fetched from there. The new deps function should look like:

defp deps do
  [
    {:phoenix, github: "phoenixframework/phoenix"},
    {:cowboy, "~> 1.0"},
    {:ecto, "~> 0.2.5"},
    {:postgrex, "~> 0.6.0"}
  ]
end

We can now run mix do deps.get, compile to fetch these dependencies and compile them. You will not that the phoenix dependency uses a keyword list as its second argument with the key github this tells mix to fetch the dependency from the GitHub repository phoenixframework/phoenix

Starting ecto

Even though Ecto and Postgrex have been imported, that doesn't mean that they are running. We need to start them.

TODO: explain the concepts in http://elixir-lang.org/getting_started/mix_otp/5.html#5.2-understanding-applications

In order to start Ecto and Postgrex we need to update the application function mix.exs to:

  def application do
    [mod: {Blog, []},
     applications: [:phoenix, :cowboy, :logger, :postgrex, :ecto]]
  end

Configuring the database

Now that we have Ecto available to us, we can generate a repository - Ecto defines this as a wrapper around the database. Ecto comes with a mix task to generate a repository. The list of available mix tasks for a project can be seen by running mix help

gazler@gazler-desktop:~/development/elixir/blog$ mix help
mix                    # Run the default task (current: mix run)
...
mix ecto.create        # Create the database for the repo
mix ecto.drop          # Drop the database for the repo
mix ecto.gen.migration # Generates a new migration for the repo
mix ecto.gen.repo      # Generates a new repository
mix ecto.migrate       # Runs migrations up on a repo
mix ecto.rollback      # Reverts migrations down on a repo
...
iex -S mix             # Start IEx and run the default task

More information on an individual task can be seen by running mix help TASKNAME

gazler@gary-desktop:~/development/elixir/blog$ mix help ecto.gen.repo

Generates a new repository.
The repository will be placed in the lib directory.

Examples
> mix ecto.gen.repo Repo

Since our application is called Blog our repository will be called Blog.Repo we can run mix ecto.gen.repo Blog.Repo to generate this.

By default, a generated repo expects the url function to be filled in by the user to point to the database. Since the location of the database is up to the developer, it should not be dictated by the project. Instead of defining the database url directly in this file, we will allow this to be specified in a config file.

Testing the database repository

It is important that this works reliably, so we will add some tests to ensure it works as intended. Elixir comes with its own test framework ExUnit.

In order to write some tests, we need to create a file to put them in. The mix test task will run all the tests in the test directory that end in _test.exs. Create the file test/blog/repo_test.exs with the following test:

    defmodule Blog.RepoTest do
      use ExUnit.Case

      test "conf uses application config if defined" do
        config = [
          username: "user",
          password: "pass",
          hostname: "localhost",
          database: "testdb",
          port: 5342
        ]
        Application.put_env(:ecto, Blog.Repo, config)
        assert Blog.Repo.conf == config
      end
    end

This tests that the application uses the config specified by Application.put_env. To make this test pass, we can do replace the contents of lib/blog/repo.ex with:

defmodule Blog.Repo do
  use Ecto.Repo, adapter: Ecto.Adapters.Postgres
  require Logger

  def conf do
    Application.get_env(:ecto, Blog.Repo)
  end

  def priv do
    app_dir(:blog, "priv/repo")
  end
end

Mix configuration

You may be wondering what Application.get_env does. Phoenix utilizes the Mix.Config module to define the config for the Application. If you look inside the config directory then you will see a number of files. We are going to add a new file for configuring the database. We will call this file database.exs

use Mix.Config

config :ecto, Blog.Repo,
  username: "user",
  password: "password",
  hostname: "localhost",
  port: 5432,
  database: "blog_development"

The config function we use here comes from Mix.Config where :ecto is the application, Blog.Repo is the key and the rest is a keyword list of options. The keys match to the config that Ecto expects to be returned from the conf function.

Even though we have added this file, we still need to include it. You will remember earlier that we said that the location of the database should be up to the developer. To ensure this is the case, we don't actually want this database.exs file to exist in the repository. What we will do instead is create a copy of it that we expect to developer to copy back to database.exs we will then remove database.exs from version control so that we don't expose our database credentials to the world.

  1. Make a copy of database.exs cp database.exs database.exs.example
  2. Add database.exs to .gitignore file echo "config/database.exs" >> .gitignore

The last thing we need to do is load our new database.exs file into the config. Add the following to the bottom of config/config.exs

import_config "database.exs"

The database can now be created by running mix ecto.create

Starting a repository

In order to use an Ecto repository, it needs to be started. We want to ensure that when our web application is running, Ecto is running. To do this we add it to our supervision tree. Update lib/blog.ex to the following:

defmodule Blog do
  use Application

  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      worker(Blog.Repo, [])
    ]

    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Blog.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

The code changes in this commit can be seen at https://github.com/Gazler/elixir-blog/commit/feb75c387b7e44908cdb94c8ea0f6926fea59ce5

Creating the post model

When you generate a Phoenix application, a web/models directory is created. This is where we will put our Ecto models. An Ecto model is an Elixir representation of a database record. A database record is stored in a table. In order to store a record in a table, we first need to create a table. To ensure that all developers create have access to this table, we will create a migration for it. You can create a migration by calling mix ecto.gen.migration Blog.Repo add_posts_table

This will create a file in the priv/blog/migrations directory. This is the directory that is specified in the priv function in our Blog.Repo module. The body of this function was generated when we called mix.gen.repo earlier.

A migration has two functions defined:

  • up - migrating from an early point to a later point
  • down - migrating from a later point to an earlier one

In this case, we want to create a posts table on the up migration and destroy it on the down migration. Populate the priv/repo/migrations/TIMESTAMP_add_posts_table.exs migration with:

defmodule Blog.Repo.Migrations.AddPostsTable do
  use Ecto.Migration

  def up do
    "CREATE TABLE if NOT EXISTS posts(
      id serial primary key,
      name text,
      created_at timestamp default CURRENT_DATE,
      updated_at timestamp
    )"
  end

  def down do
    "DROP TABLE posts"
  end
end

We can then perform the migration by running mix ecto.migrate Blog.Repo this will create the table in the database.

Now that we have the table, we can create the matching Ecto model. Create the file web/models/post.ex with the following:

defmodule Blog.Post do
  use Ecto.Model

  schema "posts" do
    field :name
    field :created_at, :datetime
    field :updated_at, :datetime
  end
end

You will notice that the schema matches the table name "posts" and the fields that we defined in the migration. The id field is not mentioned in the schema as this is generated by Ecto by default.

Now that we have the model and the table, we should be able to create a blog post. We will do this in the elixir console. Open up an elixir console using iex -S mix and do the following:

iex(1)> post = %Blog.Post{name: "My First Post"}
%Blog.Post{created_at: nil, id: nil, name: "My First Post", updated_at: nil}
iex(2)> Blog.Repo.insert(post)
%Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11,
  sec: 0, year: 2014}, id: 2, name: "My First Post", updated_at: nil}

You will notice that the updated_at field doen't have a time in them. We can use the Ecto.DateTime.utc function to get the current time. To update the updated_at field do:

iex(3)> post = Blog.Repo.get(Blog.Post, 1)
%Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11,
  sec: 0, year: 2014}, id: 1, name: "My First Post", updated_at: nil}
iex(4)> post = %{post | updated_at: Ecto.DateTime.utc}
%Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11,
  sec: 0, year: 2014}, id: 1, name: "My First Post",
 updated_at: %Ecto.DateTime{day: 14, hour: 16, min: 6, month: 11, sec: 19,
  year: 2014}}

We can validate that the created_at field has been updated by calling:

iex(6)> Blog.Repo.get(Blog.Post, 1)
%Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11,
  sec: 0, year: 2014}, id: 1, name: "My First Post",
 updated_at: %Ecto.DateTime{day: 14, hour: 16, min: 6, month: 11, sec: 19,
  year: 2014}}

Having to manually set the updated_at field to be the current time whenever we create a new record is not ideal. We should also default this to the timestamp when the record is created. Let's modify the migration to do that. Change the up migration to:

  def up do
    "CREATE TABLE if NOT EXISTS posts(
      id serial primary key,
      name text,
      created_at timestamp default CURRENT_DATE,
      updated_at timestamp default CURRENT_DATE
    )"
  end

Now run mix ecto.rollback to run the down migration. This will delete the table and the posts we created. Now run mix ecto.migrate again. This will run the up migration but now the updated_at field will default to the creation timestamp. We can validate this has worked in an iex session.

iex(1)> Blog.Repo.insert(%Blog.Post{name: "A Blog Post"})
%Blog.Post{created_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11,
  sec: 0, year: 2014}, id: 1, name: "A Blog Post",
 updated_at: %Ecto.DateTime{day: 14, hour: 0, min: 0, month: 11, sec: 0,
  year: 2014}}

You may also find the following repo functions helpful:

  • Blog.Repo.all(Blog.Post) returns all of the blogs posts
  • Blog.Repo.delete(%Blog.Post{id: 1}) will delete the post with id 1
  • Blog.Repo.delete_all(%Blog.Post) will delete all posts

The code changes for this commit can be seen at https://github.com/Gazler/elixir-blog/commit/b7ea7f4f215bed1faf2a8923ff83b72998da3ebc

Creating a web page for the posts

Now that we have a posts model that can store posts in the database, we want a way of displaying. Create a few posts in the database inside the console so we have some test data to display.

The posts will sit in our web application at the /posts route. In order for this to work, we need to add this route to the Phoenix router. Since we will be able to create, read, update and delete posts, we will use the resources function available in the Phoenix.Router module.

Add the following to the web/router.ex file under the get "/", Blog.PageController, :index line:

    resources "posts", Blog.PostsController

This will create several web endpoints for our application. You can see the routes by running mix phoenix.routes the output should look like this:

 page_path  GET     /                Blog.PageController.index/2
posts_path  GET     /posts           Blog.PostsController.index/2
posts_path  GET     /posts/:id/edit  Blog.PostsController.edit/2
posts_path  GET     /posts/new       Blog.PostsController.new/2
posts_path  GET     /posts/:id       Blog.PostsController.show/2
posts_path  POST    /posts           Blog.PostsController.create/2
posts_path  PATCH   /posts/:id       Blog.PostsController.update/2
            PUT     /posts/:id       Blog.PostsController.update/2
posts_path  DELETE  /posts/:id       Blog.PostsController.destroy/2

You can read more about Phoenix Routing at https://github.com/lancehalvorsen/phoenix-guides/blob/master/C_routing.md

The resources function has created all of the posts_path routes for us. You can see the controller that they map to, which we passed as the second argument. Let's create this controller in web/controllers/posts_controller.ex with the following contents:

defmodule Blog.PostsController do
  use Phoenix.Controller

  plug :action
  plug :render

  def index(conn, _params) do
    conn
  end
end
  • For a description of plug :action see https://github.com/lancehalvorsen/phoenix-guides/blob/master/D_controllers.md
  • For a description of plug :render see https://github.com/lancehalvorsen/phoenix-guides/blob/master/D_controllers.md#rendering

The render plug will look for a Phoenix.View file in the path web/views/posts_view.ex. Create that file with the following content:

defmodule Blog.PostsView do
  use Blog.View
end

This view simply extends the Blog.View module that was generated by Phoenix. We don't need any additional functions in this file just now.

The next thing we need is a template. Because the action in our controller is called index it is expected that this file is called web/templates/posts/index.eex Create it with the following contents:

<h2>Posts</h2>

Start the application with mix phoenix.start and navigate to http://localhost:4000/posts and you should see a page with the Phoenix logo and the word "Posts" on there.

Displaying the posts

We now have a place to display our posts. To do this, we need to pass the posts through to the template from the controller. Update the Blog.PostsController module with the following index action:

def index(conn, _params) do
  conn
  |> assign(:posts, posts)
end

defp posts, do: Blog.Repo.all(Blog.Post)

The assign function takes two arguments:

  • A symbol in this case :posts - this is the name of the variable that will be available in the template.
  • A value, in this case we call the private function posts which returns all the posts like we did in the iex session earlier. in this case :posts - this is the name of the variable that will be available in the template.
  • A value, in this case we call the private function posts which returns all the posts like we did in the iex session earlier.

We can now use this value in the posts template. Add the following to web/templates/posts/index.html.eex

<ul class="list-group">
  <%= for post <- @posts do %>
    <li class="list-group-item"><%= post.name %></li>
  <% end %>
</ul>

Now when you navigate to http://localhost:4000/posts you should see all the test posts you created earlier.

Changing the layout

Our posts page still have the Phoenix logo and footer on there. This is because we are using the application layout that was created when we ran the Phoenix generator. To change this we will create a new layout. Create a file web/templates/layout/main.html.eex with the following contents:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Blog</title>
    <link rel="stylesheet" href="/css/phoenix.css">
  </head>

  <body>
    <div class="container">
      <%= @inner %>
    </div>
  </body>
</html>

We now need to update the index action to point to this layout. Update the index action to the following:

  def index(conn, _params) do
    conn
    |> put_layout(:main)
    |> assign(:posts, posts)
  end

When you visit the page now, you will no longer see the Phoenix header and footer.

The code changes in this commit can be seen at https://github.com/Gazler/elixir-blog/commit/2225befc4c886983540a0d3592b46bc19f8667a9