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.
Since the blog application is built using the Phoenix framework, the first step is to generate a new Phoenix application.
- Get phoenix by running
git clone git@github.com:phoenixframework/phoenix.git
cd phoenix
- Get the dependencies and compile
mix do deps.get compile
- 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.
cd ../blog
mix do deps.get, compile
mix phoenix.server
- 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.
git init
git add .
git commit
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
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
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.
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
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.
- Make a copy of database.exs
cp database.exs database.exs.example
- 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
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
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 pointdown
- 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 postsBlog.Repo.delete(%Blog.Post{id: 1})
will delete the post with id 1Blog.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
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.
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.
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