chrismccord

chrismccord

Creator of Phoenix

Phoenix v1.3.0-rc.0 released

As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure, first class umbrella project support, and scaffolding that re-enforces phoenix as a web-interface to your greater Elixir application. We have also included a new action_fallback feature in Phoenix.Controller that allows you to translate common datastructures in your domain to valid responses. In practice, this cleans up your controller code and gives you a single plug to handle otherwise duplicated code paths. It is particularly nice for JSON API controllers.

For those interested in a detailed overview of the changes and design decisions, check out my LonestarElixir keynote that just went live: https://www.youtube.com/watch?v=tMO28ar0lW8

To use the new phx.new project generator, you can install the archive with the following command:

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

As always, we have an upgrade guide with detailed instructions for migrating from 1.2.x projects: Phoenix 1.2.x to 1.3.0 Upgrade Instructions · GitHub

1.3.0 is a backwards compatible release, so upgrading can be as easy as bumping your :phoenix dep in mix.exs to “~> 1.3”. For those wanting to adopt the new conventions, the upgrade guides will take you step-by-step. Before you upgrade, it’s worth watching the keynote or exploring the design decisions outlined below.

Phoenix 1.3 – Design With Intent

The new project and code generators take the lessons learned from the last two years and push folks towards better design decisions as they’re learning. Changes to new projects include moving web inside lib to avoid any special directories, as well as the introduction of the new Web namespace convention to further signal that your Phoenix related web modules are the web interface into your greater Elixir application. Along with new project structure, comes new phx.gen.html and phx.html.json generators that adopt these goals of isolating your web interface from your domain.

Contexts

When you generate a HTML or JSON resource with phx.gen.html|json, Phoenix will generate code inside a Context, which is simply a well-named module, with well-named functions representing an API boundary to part of your application domain.

For example, to generate a “user” resource we’d run:

$ mix phx.gen.html Accounts User users email:string:unique

Notice how “Accounts” is a new required first parameter. This is the context module where your code will live that carries out the business logic of user accounts in your application. Here’s a peek at part of the code that’s generated:

# lib/my_app/web/controllers/user_controller.ex
defmodule MyApp.Web.UserController do     
  ...
  alias MyApp.Accounts
  
  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, "index.html", users: users)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "user created successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end
  ...
end


# lib/my_app/accounts/accounts.ex

defmodule MyApp.Accounts do
  @moduledoc """
  The boundary for the Accounts system.
  """
  alias MyApp.Accounts.User
  
  def list_users do
    Repo.all(User)
  end
  
  def create_user(attrs \\ %{}) do
    %User{}
    |> user_changeset(attrs)
    |> Repo.insert()
  end
  ...
end

You will also have an Ecto schema generated inside lib/my_app/accounts/user.ex. Notice how our controller calls into an API boundary to create or fetch users in the system. We are still using Ecto, but the database and application logic are separated. Our web-interface doesn’t need to know the details of the storage or DB representatin of our User. The Accounts module could internally store users in an agent, ETS, or elsewhere and our controller code remains untouched.

By asking users to think about the boundaries of their APIs, we end up with more maintainable, well structured code. Additionally, we can get a glimpse of what the application does and its feature-set just be exploring the application directory structure:

lib/my_app
├── accounts
│   ├── user.ex
│   └── accounts.ex
├── sales
│   ├── ticket.ex
│   ├── manager.ex
│   └── sales.ex
├── repo.ex
└── web
    ├── channels
    ├── controllers
    ├── templates
    └── views

With just a glance at the directory structure, we can see this application has a User Accounts system, as well as sales system. We can also infer that there is a natural boundary between these systems thru the sales.ex and accounts.ex modules. We can gain this insight without seeing a single line of code. Contrast that to the previous web/models, which did not reveal any relationship between files, and mostly reflected your database structure, providing no insight on how they actually related to your domain.

We are excited about these changes and their long-term payoff in maintainability. We also feel they’ll lead to sharable, isolated libraries that the whole community can take advantage of – inside and outside of Phoenix related projects.

If you have issues upgrading, please find us on elixir-lang irc or slack and we’ll get things sorted out!

I would also like to give a special thank you to @wojtekmach for his help getting the new generators ready for prime-time. <3

Happy coding! :front_facing_baby_chick::fire:

-Chris

Full changelog:

1.3.0-rc.0 (2017-03-01)

  • Enhancements

    • [Generator] Add new phx.new, phx.new.web, phx.new.ecto
      project generators with improved application structure and support for
      umbrella applications
    • [Generator] Add new phx.gen.html and phx.gen.json resource
      generators with improved isolation of API boundaries
    • [Controller] Add current_path and current_url to generate a
      connection’s path and url
    • [Controller] Introduce action_fallback to registers a plug to
      call as a fallback to the controller action
    • [Controller] Wrap exceptions at controller to maintain connection
      state
    • [Channel] Add ability to configure channel event logging with
      :log_join and :log_handle_in options
    • [Channel] Warn on unhandled handle_info/2 messages
    • [Channel] Channels now distinguish from graceful exits and
      application restarts, allowing clients to enter error mode and
      reconnected after cold deploys.
    • [Router] document match support for matching on any http method
      with the special :* argument
    • [ConnTest] Add redirected_params/1 to return the named params
      matched in the router for the redirected URL
  • Deprecations

    • [Generator] All phoenix.* mix tasks have been deprecated in
      favor of new phx.* tasks
  • JavaScript client enhancements

    • Add ability to pass encode and decode functions to socket
      constructor for custom encoding and decoding of outgoing and incoming
      messages.
    • Detect heartbeat timeouts on client to handle ungraceful
      connection loss for faster socket error detection
    • Add support for AMD/RequireJS

Most Liked

josevalim

josevalim

Creator of Elixir

It is not new. Phoenix projects already do not follow the path conventions on entries such as web/controllers/user_controller.ex (which would have required its module to be named MyApp.Web.Controllers.UserController).

Mix itself keeps Mix.Tasks.Escript.Build in lib/mix/tasks/escript.build.ex instead of lib/mix/tasks/escript/build.ex. We do it so you can quickly glance into lib/mix/tasks and see all available tasks.

Not caring about the directory structure has been an Elixir feature from before 1.0. Different projects have leveraged it to provide better code organization under circumstances they find it worth.

In this case, we find that colocating the context file with other files that belong to the same context is going to help exploration. Contrast how it works now:

    + accounts
      - accounts.ex
      - foo.ex
      - bar.ex
    + backoffice
    + blog
    + payments
    + sales
    + web

with the old way

    + accounts
      - foo.ex
      - bar.ex
    + backoffice
    + blog
    + payments
    + sales
    + web
    - accounts.ex
    ...
    - sales.ex

We find the first easier to explore if the files are all in one place. Chris even had to “cheat” in his presentation by making those files visually together while most tree viewers wouldn’t put them side by side. And yes, the idea is transferable to the whole ecosystem.

Therefore it is totally fine if you follow Phoenix conventions elsewhere, especially if you believe they lead to a better code structure. And if you want to try other approaches than the ones mentioned above, then discuss it with your team and go ahead with your changes if you are in agreement.

chrismccord

chrismccord

Creator of Phoenix

Thanks for the feedback! Answers inline.

21 functions is not a huge API surface area. I can understand the concern of God Modules, but as previous posts have shown, it’s difficult to make suggestions if a module is doing too much without concrete use cases. Also note that just because you have 3 entities in your context, it does not necessarily mean you’ll have the same conventional functions for all of them. Sometimes you will, sometimes you won’t. For example, imagine a a bank system with a Deposit, an Account, and a AccountHolder. Under this case, you likely wouldn’t expose the crud functions for deposits at all, instead delegating the details of that deeper into the system. That’s not to say you won’t end up with > 21 functions. As José has said, it’s easier to start with the parts of your app in the same place, then see where they can be separated than trying to predict the future up front. If these parts of your app are necessarily coupled and depend on one another, grouping together their functionality is not a problem. If you have specific examples we can try to say more.

There is duplication here if all your entities are using ecto, but I don’t feel this is a good candidate for code injection. use GenServer is injecting code to satisfy a behaviour with known functionality. The point of your contexts is that they are the boundary to the internal details of listing users/roles/permissions. If your user permissions are stored in ets, but the users and roles in Postgres, then you can’t just inject code. I also would prefer to see an explicit call when viewing the module. If you want to offload the query generation that you handle the same way, then I would do something like this:

def list_users(opts \\ []) do
  opts
  |> QueryBuilder.apply_opts(User)
  |> Repo.all()
end

def list_permissions(user, opts) do
  from(p in assoc(user, :permissions))
  |> QueryBuilder.apply_opts(opts)
  |> Repo.all()
end

Notice how even using your user and permission example, our “duplicated” api for listing resources already contains some differences. For example, list/create/update/delete_permissions is going to take the user as an argument, where the generated functions won’t have such scoping. I don’t feel code injection is best here, but there are still ways to offload shared querying by delegating outside, as QueryBuilder in my example that would know how to build an Ecto query given some options.

Changests can be for any source, and I usually go the other way and collocate them close the user input or changes I am modeling. That said, there is nothing stopping you from collocating changeset functions with the schema. It’s a valid option if that suits you, but since my boundaries expose my Schema structs publicly, I like to keep private things like changeset building out of the module.

This is still a good approach, but since Ecto achieved parallel database transactions, this design is no longer an essential part of your testing strategy. I actually find it easier to write and maintain tests that simply hit the database internally as needed than those that go through hoops to manage or stub db calls.

The first part of this sentence is absolutely true – the new context generators will result in more code than throwing a Repo.insert in the controller, but as far as maintenance is concerned seeing the LOC that the new generators build vs the old ones is not a good maintenance indicator. For example, every time you duplicate Repo.insert / Ecto.build_assoc/ etc calls in multiple controllers, channels, other modules, etc, you are placing a future burden on yourself. Compound this over a few years, and it will be a massive maintenance cost. Imagine needing to offload persistence of parts of the system to another store, or another app entirely? Now you have to refactor every part of the system that this “simpler” code touched.

Every decision we make in software is a trade-off, but I feel the current approach maintenance wise is an absolute win, with the potential for slightly more effort up front. We feel that effort is going to be worth it :slight_smile:

ion

ion

Thank you to everyone who worked on this project!

Where Next?

Popular in Phoenix News Top

steffend
LiveView 1.2.0 is out now! Colocated CSS The biggest new feature is support for Colocated CSS, which is built on the work we did in 1.1 ...
New
New
chrismccord
A minor vulnerability has been disclosed for applications redirecting to URLs provided by user input. Only applications passing user inpu...
New
chrismccord
The final release of Phoenix 1.7 is out! Most of the new features have been outlined in the 1.7 RC thread, but it has been a few months s...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31013 112
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
chrismccord
Phoenix 1.3.5, 1.4.18, 1.5.14, and 1.6.14 have been released to resolve a vulnerability in wildcard check_origin configurations. Previou...
New
chrismccord
Check the announcement blog for details! Blog duped here for convenience: Phoenix 1.8.0-rc released! The first release candidate of P...
418 13659 167
New
chrismccord
Hey folks, Phoenix 1.3.0 is out! The final release please brings tweaks to web directory and alias conventions that were estabished in t...
New
chrismccord
LiveView 1.0.0-rc.0 is out! This 1.0 milestone comes almost six years after the first LiveView commit. I published a Phoenix blog highli...
New

Other popular topics Top

TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41989 114
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43487 311
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39467 209
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement