Off the top of my head while its fresh here are my thoughts on it. This won’t be an exhaustive list of changes because I’m lazy and on vacation. So I’m just going to pull the most notable features from memory, which I think has its own sort of value in that these were the most memorable to someone focused primarily on developing the Elixir side of an app.
In Phoenix 1.3 the /web
folder has been moved inside /lib/<project>/web
to be more in line with a typical Mix application. To anyone used to a Phoenix project this would be the most immediately noticeable change. Along with this change, all of your controllers and views will also be namespaced inside of Web
. For example, the standard Project.PageController
that comes with the generator becomes Project.Web.PageController
, and the Project.PageView
becomes Project.Web.PageView
My first impression of this change is that Phoenix is trying to become more in line with traditional Elixir/Erlang OTP app structures, including their Supervision tree structure and I support that 100%.
In the keynote where these changes were announced, they talked about visualizing your Phoenix code as just one way to interact with your underlying application, which could have many other ways. This is already true even within Phoenix. If your application has an API and a UI, then you most likely have multiple avenues of achieving the same result. Bringing this out explicitly is a huge win for developers.
One of the ways that Phoenix 1.3 makes this explicit is also the next big change in the app. Now, when you generate a new resources (whether through gen.html
or gen.shema
[the gen.model
replacement]), you also have to specify a Context. From the docs:
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).
To me, this is where 1.3 really shines. When I was first wrapping my head around it, I tended to use Domain a lot in my head instead of Context, which was helpful to me. When you generate a new resource, the context is also generated for you, along with an outline of the functions that probably belong there.
defmodule Project.Accounts do
@moduledoc """
The boundary for the Accounts system.
"""
import Ecto.{Query, Changeset}, warn: false
alias Project.Repo
alias Project.Accounts.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)
...
end
This lends itself perfectly to building an application around multiple access points to your data. It’s also something that I haven’t seen in any other frameworks I’ve worked with. This sort of organization is typically left as an exercise for the user.
Here is how the Contexts ended up influencing my app design.
I was building an app that had multiple “accounts” that it needed to track, so I had an Accounts.User
, I had a Github.User
, and I had a Slack.User
, each responsible for storing its own data. Inside each of those contexts were the functions I needed to work with the resources it contained.
For example, I needed to be able log in and register as an Accounts.User
with Guardian, so these functions got added to the context:
def authenticate(params) do
find_from_auth(params)
|> validate_password(params["password"])
end
def register(%{"password" => pass, "password_confirmation" => conf} = params) when pass == conf do
do_registration(params)
end
def register(params) do
{:error, "Passwords do not match"}
end
In my Slack.User
, I needed ways to associate it to an Accounts.User
and so I had helper functions over there as well. I had function in Github.User
for maintaining the link between my user and their accounts api. I also built a had a Settings
context for a user, and the Settings
context knew how to load the settings applicable to the whatever model was provied. I wanted Slack.Users
settings to be aware of Slack Channels and teams as well as just the user, and the context provided a good place to house these separate semantics.
For me, context’s a very welcome abstraction. In my previous Phoenix project it was always a bit confusing as to whether something belonged in /web
or in /lib
. That project grew to be pretty hefty, and ended up having a lib/data_store/
folder which was vaguely similar to what Contexts provide. What I was reaching for was a place to hold the code that in an OO framework like Rails would be shoved on to the model. I love the Repo pattern that Phoenix uses but I did not love include
ing Ecto.Query
everywhere that I needed to lookup a record. Contexts provide a clear place for holding that code in an Elixir way.
Taken together, I think contexts and the move of web
into /lib/project
is a clear win. It leads to a more well organized project and in the end I think it will save many headaches. I think it is a project structure that provides clear avenues for growth. Having that structure by default, rather than solving for a simpler use case, really sets Phoenix apart.
I’m very excited to keep building stuff with Elixir and Phoenix. From an outsiders perspective the team has really taken on the hard challenges head on and really moved forward with them.
That new project I build is a bridge to work with GitHub Issues inside of Slack, you should check it out.
💘😽🎉