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
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.
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
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
Accountscontext (which is under
lib/alloy_ci/accountsfolder), where the
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
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.
The code that will actually present your data to the user can live under the
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.
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
Everything that comes after
"show.html" are the assigns, so the variables available to the templates related to the
show action are
current_user. We can see an example of how to use them in this snippet from the pipeline info header:
Once a variable has been assigned, it is available to the template via
@var_name, just like with Rails. Functions defined 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.
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 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.
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 receive 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).
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.