The Phoenix Framework has always been awesome. But he has never been as cool as with the new release 1.3 (which is currently in the RC2 stage).
There have been many significant changes. Chris McCord has written a complete guide to change . His speech with LonestarElixir is also available, where he tells in detail about the key points. Inspired by his works, in my article I will try to tell you about the most important changes in the Phoenix project.
Let's start!
The translation was made by the author of the original article, Nikita Sobolev .
Phoenix is ββa new framework. And, of course, he has some problems. The core team worked very diligently to solve some of the most important ones. So what are these problems?
When working on a project using Phoenix, you have two places for source code: lib/
and web/
. The concept is:
lib/
.web/
.But is this clear to the developers? I do not think so.
Where does this web directory come from? Is this a Phoenix feature? Or other frameworks also use it? Should I use lib/
with Phoenix projects or is it reserved for some deep magic? All these questions came to me after my first meeting with Phoenix.
Prior to version 1.2, only the web/
directory automatically rebooted. So, why should I create any files inside lib/
and restart the server when I can put them somewhere inside the web/
for a quick reload?
This leads us to even more important questions: do my model files (let's call them models in this particular context) relate to the web
application or to the basic logic? Is it possible to divide the logic into different domains or applications (for example, as in Django)?
These questions remain unanswered.
Moreover, the template code that goes to Phoenix suggests another way. You can get the following code in a new project:
defmodule Example.UserController do use Example.Web, :controller # ... def update(conn, %{"id" => id, "user" => user_params}) do user = Repo.get!(User, id) changeset = User.changeset(user, user_params) case Repo.update(changeset) do {:ok, user} -> render(conn, Example.UserView, "show.json", user: user) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render(Example.ChangesetView, "error.json", changeset: changeset) end end end
What should a developer do when an email is sent to the user after a successful update? The controller asks to be expanded. Just put one more line of code before render/4
, what could go wrong? But. Phoenix has just pushed us to misuse its codebase: we write business logic in the controller!
In fact, one extra line in the controller is normal. All problems arise when the application grows. There are many such lines, the application becomes unstable, unaffordable and repeats itself.
At some point, for no particular reason, the Ecto
schemes became known as βmodelsβ. What is the difference between a βmodelβ and a βschemeβ? A schema is just a way to define a structure β a database structure in this particular case. Models as a concept are much more complex than schemes. Models must provide a way to manage data and perform various actions, like models in Django or Rails. Elixir as a functional language is not suitable for the concept of "model", so they were abolished in the project Ecto
.
Files inside the models/
were not organized. As it grows, your application becomes chaotic. How are these files related? In what context do we use them? It was hard to understand.
In addition, the models/
directory was considered as another place to put your business logic, which is normal for other languages ββand frameworks. There is the familiar concept of "fat models". But this concept, again, is not suitable for Phoenix for the reasons already mentioned.
Since the last major release, much has changed. The easiest way to show all changes is by example.
This guide assumes that you have elixir-1.4
, and it works. Not? So install it !
First you need to install a new version of Phoenix:
mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
Upon completion of the installation, you need to check whether everything is in place. mix help
will give you something like this:
mix phoenix.new # Creates a new Phoenix v1.1.4 application mix phx.new # Creates a new Phoenix v1.3.0-rc.1 application using the experimental generators
This is where the first change manifests itself: the new generators. The old generators were called phoenix
, and the new ones were just phx
. Now you need less typing. And, more importantly, a new message to developers: these generators are new, they will do something new for your project.
Then you need to create a new project structure by running:
mix phx.new medium_phx_example --no-html --no-brunch
Before we see any results of this command, let's discuss the parameters. --no-html
removes some components for working with html
, so phx.gen.html
will no longer work. But we build json
API, and we do not need html
. Similarly, --no-brunch
means: do not create a brunch
file for working with statics.
Looking at your new files, you may wonder: where is the web directory? Well, here is the second change. And pretty big. Now your web directory is inside lib/
. It was special, many people misunderstood its main goal, which was to keep the web interface for your application. This is not the place for your business logic. Now everything is clear. Put everything inside lib/
. And leave only your controllers, templates and views inside the new web directory. Here's what it looks like:
lib βββ medium_phx_example βββ application.ex βββ repo.ex βββ web βββ channels β βββ user_socket.ex βββ controllers βββ endpoint.ex βββ gettext.ex βββ router.ex βββ views β βββ error_helpers.ex β βββ error_view.ex βββ web.ex
Where medium_phx_example
is the name of the current application. Applications can be many. So now all the code lives in the same directory.
The third change will open shortly after viewing the web.ex
file:
defmodule MediumPhxExample.Web do def controller do quote do use Phoenix.Controller, namespace: MediumPhxExample.Web import Plug.Conn # Before 1.3 it was just: # import MediumPhxExample.Router.Helpers import MediumPhxExample.Web.Router.Helpers import MediumPhxExample.Web.Gettext end end # Some extra code: # ... end
Phoenix now creates a .Web
namespace that .Web
very well with the new file structure.
This is the fourth and my favorite change. Previously, we had a web/models/
directory web/models/
, which was used to store the schemes. Now the concept of the models is completely dead. Introduced a new philosophy:
Our application will contain only one context: Audio
. Let's start by creating an Audio
Context with two Album
and Song
schemes:
mix phx.gen.json Audio Album albums name:string release:utc_datetime mix phx.gen.json Audio Song songs album_id:references:audio_albums name:string duration:integer
The syntax of this generator has also changed. Now it is required that the context name be the first argument. Also note the audio_albums
, the schemes now contain a prefix with the context name. And this is what happens with the project structure after running two generators:
lib βββ medium_phx_example βββ application.ex βββ audio β βββ album.ex β βββ audio.ex β βββ song.ex βββ repo.ex βββ web βββ channels β βββ user_socket.ex βββ controllers β βββ album_controller.ex β βββ fallback_controller.ex β βββ song_controller.ex βββ endpoint.ex βββ gettext.ex βββ router.ex βββ views β βββ album_view.ex β βββ changeset_view.ex β βββ error_helpers.ex β βββ error_view.ex β βββ song_view.ex βββ web.ex
What are the main changes in structures compared to the previous version?
web/
, and the models/
directory has disappeared altogether.And the diagrams right now are nothing more than a description of the table. What should be the scheme in the first place. Here are our schemes:
defmodule MediumPhxExample.Audio.Album do use Ecto.Schema schema "audio_albums" do field :name, :string field :release, :utc_datetime timestamps() end end
defmodule MediumPhxExample.Audio.Song do use Ecto.Schema schema "audio_songs" do field :duration, :integer field :name, :string field :album_id, :id timestamps() end end
Everything except the scheme itself has disappeared. There are no required fields, no changeset/2
or any other functions. The generator now doesn't even create belongs_to
for you. You yourself manage the connections of your schemes.
So now this is pretty clear: the schema is not the place for your business logic. All of this is handled by the context, which looks like this:
defmodule MediumPhxExample.Audio do @moduledoc """ The boundary for the Audio system. """ import Ecto.{Query, Changeset}, warn: false alias MediumPhxExample.Repo alias MediumPhxExample.Audio.Album def list_albums do Repo.all(Album) end def get_album!(id), do: Repo.get!(Album, id) def create_album(attrs \\ %{}) do %Album{} |> album_changeset(attrs) |> Repo.insert() end # ... defp album_changeset(%Album{} = album, attrs) do album |> cast(attrs, [:name, :release]) |> validate_required([:name, :release]) end alias MediumPhxExample.Audio.Song def list_songs do Repo.all(Song) end def get_song!(id), do: Repo.get!(Song, id) def create_song(attrs \\ %{}) do %Song{} |> song_changeset(attrs) |> Repo.insert() end # ... defp song_changeset(%Song{} = song, attrs) do song |> cast(attrs, [:name, :duration]) |> validate_required([:name, :duration]) end end
The very context view sends a clear message: here is the place to put your code! But be careful, context files can grow. Separate them into several modules in this case.
Previously, we had a lot of code in the controller by default and it was easy for the developer to extend the template code. Here is the fifth change. Starting with the new release, the template code in the controller has been reduced and reorganized:
defmodule MediumPhxExample.Web.AlbumController do use MediumPhxExample.Web, :controller alias MediumPhxExample.Audio alias MediumPhxExample.Audio.Album action_fallback MediumPhxExample.Web.FallbackController # ... def update(conn, %{"id" => id, "album" => album_params}) do album = Audio.get_album!(id) with {:ok, %Album{} = album} <- Audio.update_album(album, album_params) do render(conn, "show.json", album: album) end end # ... end
In action update/2
now there are only three meaningful lines of code.
Currently, controllers use contexts directly, which makes them a very thin layer in the application. It is very difficult to find a place for additional logic in the controller. What was the main task during the reorganization.
Controllers do not even handle errors. A special new fallback_controller
intended for working with errors. This new concept is the sixth change. It allows you to have all error handlers and error codes in one place:
defmodule MediumPhxExample.Web.FallbackController do @moduledoc """ Translates controller action results into valid `Plug.Conn` responses. See `Phoenix.Controller.action_fallback/1` for more details. """ use MediumPhxExample.Web, :controller def call(conn, {:error, %Ecto.Changeset{} = changeset}) do conn |> put_status(:unprocessable_entity) |> render(MediumPhxExample.Web.ChangesetView, "error.json", changeset: changeset) end def call(conn, {:error, :not_found}) do conn |> put_status(:not_found) |> render(MediumPhxExample.Web.ErrorView, :"404") end end
What happens when the result from Audio.update_album(album, album_params)
does not match {:ok, %Album{} = album}
? In this situation, the controller defined in action_fallback
. And the correct call/2
will be selected, which in turn returns the correct answer. Easy and pleasant. No exception handling in the controller.
The changes made are very interesting. There are many of them; they are all focused on ruining the old habits of programmers who came from other programming languages. And new changes are trying to supplement the philosophy of Phoenix-Way
with new practices. I hope this article was helpful and prompted you to use the Phoenix Framework to the maximum. Come to my github .
We thank Nikita for preparing the translation of our own original article and gladly publish material on HabrΓ©. Nikita represents the ElixirLangMoscow community, which organizes Elixir meetings in Moscow, as well as an active contributor to open-source and makes a significant contribution to our Wunsh community . On the site you are waiting for 3 dozen feature articles, weekly newsletters and news from the world of Elixir. And for questions we have a chat in the Telegram with excellent participants.
Source: https://habr.com/ru/post/332898/
All Articles