This document exists for Elixir and Phoenix application developers to learn from the design missteps of others.
It's not a manifesto or a bible or even a style guide – it's just a list of guidelines to consider before laying out the structure of an application. With a little foresight, the cumulative effect of these individual tips will result in a consistent application structure and, hopefully, a boost in productivity.
def show(conn, %{"id" => id}) do
post = MyApp.Repo.get!(Post, id)
render(conn, "show.html", post: post)
end
def show(conn, %{"id" => id}) do
post = MyApp.Blog.get_post!(id)
render(conn, "show.html", post: post)
end
Ecto's Repo
is an implementation detail internal to your application that should not be exposed to the web layer. Data persistence can be backed by many things: a relational database, a document database, an ETS table, a flat file, an external API, a GenServer… the list goes on. Leave it up to the Context to hide that detail from your external web interface.
MyApp.Blog.update_post(params, post)
MyApp.Blog.update_post(post, params)
Elixir isn't object-oriented, but the pipe operator |>
lets us mutate data in a functional way that still feels like calling a method on an object. Take advantage of that exepectation by designing your functions to take the data to be mutated (or a pointer to that data, like an id
) as the first parameter.
# lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
import Ecto.Query
def published(query \\ Post) do
from p in query, where: not is_nil(p.published_at)
end
def ordered(query \\ Post) do
from p in query, order_by: p.published_at
end
end
# lib/my_app/blog/blog.ex
defmodule MyApp.Blog do
def list_posts do
Post
|> Post.published
|> Post.ordered
|> Repo.all
end
end
Defining simple, composable queries directly on the schema module is very convenient for building up more complex queries at higher levels.
# lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
import Ecto.Changeset
def update(post, params) do
cast(post, params, [:title, :body])
end
def publish(post) do
change(post, published_at: DateTime.now_utc)
end
end
# lib/my_app/blog/blog.ex
defmodule MyApp.Blog do
def update_post(post, params) do
post
|> MyApp.Blog.Post.update(params)
|> Repo.update
end
def publish_post(post) do
post
|> MyApp.Blog.Post.publish
|> Repo.update
end
end
Defining simple, composable changesets directly on the schema module is very convenient for building up more complex queries at higher levels.
# lib/my_app/web.ex
defmodule MyApp.Web do
# ...Phoenix-generated scopes here...
def authenticated_controller do
quote do
use MyApp.Web, :controller
# Example using Guardian
plug Guardian.Plug.EnsureAuthenticated, handler: MyApp.Web.AuthHandler
# Override Phoenix.Controller.action/2 callback
def action(conn, params) do
args = [conn, params, Guardian.Plug.current_resource(conn)]
apply(__MODULE__, action_name(conn), args)
end
end
end
end
# lib/my_app/web/controllers/post_controller.ex
defmodule MyApp.Web.PostController do
# Use the new scope
use MyApp.Web, :authenticated_controller
# Notice the extra parameter
def new(conn, params, current_user) do
changeset = MyApp.Blog.new_post(current_user, params)
render(conn, "new.html", changeset: changeset)
end
end
By overriding the default Phoenix controller action/2
callback, you can inject custom parameters into each controller action. For authenticated controllers, knowing the current_user
is often required, so pass it in directly to the function for convenience.