Structuring a new Phoenix Project
This blog post is part of a series of posts detailing the development process of AlloyCI. The previous entries have been:
If you are coming from a Rails background, you might find the folder structure of a Phoenix project a bit weird, specially after the changes in 1.3. But don’t be put off by this, the structure actually makes a lot of sense, specially the new 1.3 structure.
Unlike Rails, there is no app/
folder where most of your code will live. Phoenix 1.3 projects follow the structure of pretty much any regular Elixir project, this means that most of your code will live inside the lib/my_app
folder.
As you might now, Phoenix 1.3 changed the way projects are structured. Before this change most of your code would be under web/
, with little to no further organizations. You would have a models directory, where you would put all your business logic, without much consideration as to how they interact together. This folder would get pretty large, pretty fast. Also the name model seems to imply an object, but in Elixir, there are no objects, so storing your data access layer under a folder named "models" makes little contextual sense.
Data & Contexts
Phoenix 1.3, by default, guides you towards a better way of organizing your code. Controllers, Views, Templates go under lib/my_app_web
, database migrations and related files go under priv/repo
, and the rest of your Elixir code will live under lib/my_app
. This is the basic setup, and you can tweak it, and change it as much as you like. You have complete liberty as to how to organize your code.
Since I started writing AlloyCI before Phoenix 1.3 was fully released, some of the folder structure is different than the one the latest 1.3 generators create. I prefer the way AlloyCI is structured right now, because I really don’t like the way they renamed the web/
folder to alloy_ci_web/
and everything inside it from AlloyCi.Web.XXX
to AlloyCiWeb.XXX
. I really prefer the separation in the module name, and the fact that the app name is not repeated in the web folder name. Thanks to the flexibility Phoenix provides, I don’t need to follow the conventions, though.
Anyways, the most important part about the structure changes, is that Phoenix now guides you towards using contexts for structuring your data access layer.
Using AlloyCI as an example, we have the Accounts
context (which is under lib/alloy_ci/accounts
folder), where the User
, Authentication
, and Installation
schemas live. These 3 schemas are closely related, and belong to the same business logic, namely the handling of Accounts.
If you look closely at the files under the accounts folder, you will see that there are no functions in the schema files, other than the changeset
function. This means that I would need to either go straight through Ecto
to manipulate the database data (not recommended) or that I need an API boundary that will let me perform actions on the accounts related schemas.
This is where the AlloyCi.Accounts
module comes into play. This module is the boundary with which AlloyCI will interact if it needs to perform an action on any of the schemas related to an Account. All public functions of this module provide an easy way to manipulate the data, while providing security through a layer of indirection.
This is the purpose of contexts. They provide a boundary between your data layer and your business logic, and allow you to have an explicit contract that tells you how you can manipulate your data. It also allows you to stay independent from Ecto.
Let’s say, in the future, you’d like to switch from Ecto to the “latest, coolestDB driver”. If you didn’t use an abstraction layer, like the contexts, you would have to refactor every function across the codebase that used Ecto to communicate to the data layer. But since we are using contexts, we would only need to refactor the code inside the context itself.
Data Presentation
The code that will actually present your data to the user can live under the lib/my_app/web
or lib/my_app_web
folders, depending on how you want to structure it (the automatic generator will default to lib/my_app_web
but I prefer the former).
In here you will find the folders where your controllers, views, templates and channels will live. Let’s start with the presentation layer.
Views & Templates
If you come from a Rails background, you might wonder why there are two components to presenting the data, when in Rails all you need is the views folder. In Phoenix, the “views” are not composed of templated HTML files, but rather they are regular Elixir modules. These modules are there to help you share code with the template, and fulfill a similar purpose as the Rails “View Helpers”, but are, by default, specific to a single controller (other views are not loaded, unlike Rails that loads all view helpers, regardless of the controller being called).
This separation makes it easier to use the same signature on similar helper functions needed to present data (without really overloading them), depending on which controller is being called, thus simplifying your code.
The templates are, then, where your HTML code lives. The template files are saved as *.html.eex
files (meaning embedded Elixir), and are very similar to erb
files. The syntax is exactly the same, but instead of Ruby code inside, you write Elixir code 😄
A very important distinction between Phoenix and Rails is how you share information between the controller and the template. In Rails, it is enough to declare an instance variable with @something
and it will be available to the template/view.
Given the functional nature of Elixir, in Phoenix you need to explicitly pass the information you wish to be available to the views in the render function. These are called assigns. As an example, here is the show
action of the PipelineController
:
def show(conn, %{"id" => id, "project_id" => project_id}, current_user, _claims) do
with %Pipeline{} = pipeline <- Pipelines.show_pipeline(id, project_id, current_user) do
builds = Builds.by_stage(pipeline)
render(conn, "show.html", builds: builds, pipeline: pipeline, current_user: current_user)
else
_ ->
conn
|> put_flash(:info, "Project not found")
|> redirect(to: project_path(conn, :index))
end
end
Everything that comes after "show.html" are the assigns, so the variables available to the templates related to the show action are builds
, pipeline
, and current_user
. We can see an example of how to use them in this snippet from the pipeline info header:
<div class="page-head">
<h2 class="page-head-title">
<%= @pipeline.project.owner %>/<%= @pipeline.project.name %>
</h2>
<nav aria-label="breadcrumb" role="navigation">
<!-- Breadcrumb -->
<ol class="breadcrumb page-head-nav">
<li class="breadcrumb-item">
<%= ref_icon(@pipeline.ref) %>
<%= clean_ref(@pipeline.ref) %>
</li>
<li class="breadcrumb-item">
<%= icon("github") %>
<%= sha_link(@pipeline) %>
</li>
<li class="breadcrumb-item">
<%= icon("book") %>
<%= pretty_commit(@pipeline.commit["message"]) %>
</li>
<%= if @pipeline.commit["pr_commit_message"] do %>
<li class="breadcrumb-item">
<%= icon("code-fork") %>
<%= @pipeline.commit["pr_commit_message"] %>
</li>
<% end %>
<li class="breadcrumb-item">
<%= icon("tasks") %>
<%= String.capitalize(@pipeline.status) %>
<%= status_icon(@pipeline.status) %>
</li>
</ol>
</nav>
</div>
Once a variable has been assigned, it is available to the template via @var_name
, just like with Rails. Functions defined in the view file of the same name as the controller (in this example pipeline_view.ex
) are immediately available to the template. In the above example, sha_link/1
creates an HTML link to the specific commit on GitHub.
Controllers
In structure, Phoenix Controllers are very similar to Rails Controllers, with the main difference being described above. When generated by the helper tools, they will have the same index
, show
, edit
, update
, and delete
actions as their Rails counterparts. And just as with Rails Controllers, you can define any action you desire by defining a function, and connecting a route to it.
Channels
Phoenix Channels are used to communicate with the web client via Web Sockets. They are similar to ActionCable in Rails, but in my opinion, much more powerful, and performant. In AlloyCI, they are used to push the output of the build logs in real time, and to render a pre formatted piece of HTML code to show the user’s repositories (more on how AlloyCI uses Channels will be discussed in another post).
Routes
Routes in Phoenix are defined in a somewhat similar way as Rails Routes. Some of the syntax is different, but it is immediately recognizable and feels familiar. Have a look at how the project routes are defined on AlloyCI:
scope "/", AlloyCi.Web do
...
resources "/projects", ProjectController do
resources("/pipelines", PipelineController, only: [:create, :delete, :show])
resources("/builds", BuildController, only: [:show, :create]) do
get("/artifact", BuildController, :artifact, as: :artifact)
post("/artifact/keep", BuildController, :keep_artifact, as: :keep_artifact)
end
resources("/badge/:ref", BadgeController, only: [:index])
end
...
end
The real difference when it comes to routes, between Phoenix and Rails, is the power that you get when using plugs. We will discuss them in detail in a future post.
And there you have it. That is the basic structure of a Phoenix project. There are other components that we haven’t covered here, like Plugs, or Background Jobs. We will discuss these advanced topics in a future blog post.
Top comments (0)