πŸ“œ ⬆️ ⬇️

Why changes in the new Phoenix 1.3 are so important


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 .


Existing problems


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?


Web directory - pure magic


When working on a project using Phoenix, you have two places for source code: lib/ and web/ . The concept is:



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.


Business logic in controllers


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.


Schemes are not models


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.


Solutions


Since the last major release, much has changed. The easiest way to show all changes is by example.


Requirements


This guide assumes that you have elixir-1.4 , and it works. Not? So install it !


Installation


First you need to install a new version of Phoenix:


 mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez 

Creating a new project


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.


Changes


Web directory


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.


Creating a schema


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:


  1. the schema represents the data structure;
  2. context is used to store multiple schemas;
  3. context is used to provide a public external API. In other words, it determines what can be done with your data.

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?


  1. Now the schemes do not belong to web/ , and the models/ directory has disappeared altogether.
  2. Schemes are now separated by a context that defines how they are related to each other.

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.


Controller use


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.


Conclusion


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