Web Contexts in Phoenix Framework 1.3

Jeremy Huffman
Smart Metals Recycling
5 min readSep 3, 2017

One of the features of the Phoenix 1.3 release is the introduction of Contexts in the generator mix tasks. Contexts help you organize your application according to your domain vocabulary, and provide a clean API boundary between your web components and your core application logic. If you haven’t already watched it I’d highly recommend Chris McCord’s introductory talk on this feature at the Lonestar conference.

Among other things, Contexts help group your related files together in a folder structure. However, this only applies to the Context module and the Schemas you create — in 1.3 all of your web content is in a myapp_web folder which is organized by module type, e.g. you have a controllers folder with all your controllers, a views folder with all of your views, a templates folder and so on. In this post I’m going to demonstrate an alternative structure for your web content, organized somewhat like Contexts but focused on the controller as the central element. I think this alternative structure makes large, complicated web applications with lots of controllers easier to manage.

To provide examples throughout this post I created an application that supports a Content context with blogging features. Below is an example of the folder structure that we see in a freshly generated Phoenix 1.3 project with just a couple of schemas — in case you want to follow along:

mix phx.new bigapp
cd bigapp
mix ecto.create
mix phx.gen.context Auth User users name email
mix phx.gen.html Content BlogPost blog_posts title content:text author_id:references:users
mix phx.gen.html Content Comment comments content:text author_id:references:users blog_post_id:references:blog_posts
#in lib/bigapp_web/router.ex append to scope "/"
resources "/blog_posts", BlogPostController
resources "/comments", CommentController
mix ecto.migrate
mix phx.server
# http://localhost:4000/blog_posts works!

I create an “Auth” context which in a real application might provide some features for authentication and authorization. I’m not going to build that functionality into this example, but I still wanted a users table. A second context, “Content” is for the blog and other CMS features. After generating these modules the lib/bigappweb folder looks like this:

This works fine at this scale, but in a larger project it takes a little more time to setup your active workspace for a particular feature. When building a particular website feature we generally have a controller, a view and various templates open at the same time. Finding all the correct files to open for a given feature begins to take more time. This is particularly true of template files, which practically speaking cannot be searched for by file or module name so you generally have to browse for them.

An alternative arrangement would co-locate the controller, view and templates for a given route in the folder structure of the bigapp_web folder. Multiple controllers that work together could in turn be grouped into what I’m calling a web context, which may or may not mirror the application Context structure. Here’s an example of that transformation — the new structure appears on the right:

With one exception, all I did with these files is move and rename them. Elixir doesn’t care what we name our module sources or where we put them — as long as they are under lib with an .ex extension in this project they will get built, and when it runs, the modules will resolve based on the defmodule names which I have not changed. There is an argument to be made about whether we should have a module named BigappWeb.BlogPostController in a file named blog_post/controller.ex and certainly you could move the file to this structure and still name it blog_post_controller.ex.

The only thing we have to do in our files to make this work is a minor tweak in the __using__ macro in the bigapp_web.ex. This macro is setup in all Phoenix projects in the *_web.ex (formerly web.ex) file by the project generator and it gets invoked in each of our view modules based on their call to use BigappWeb (e.g. use BigappWeb, :view). If you aren’t familiar with Elixir macros don’t be alarmed — this is a pretty straightforward change. Basically we just need to tell Phoenix.View where to find our templates, as templates are resolved by path rather than by module name. In our new convention, instead of looking in bigapp_web/templates/view_module_inflect, the templates are always found in a directory named templates (or a sub-directory of templates) in the same directory as the View module’s file.

To support this, we add another clause above the existing __using__ macro:

In your project, just set @webdir to the correct value for yourapp_web — its the same path you’ll see in the default options passed to Phoenix.View in your generated view function.

__CALLER__.file returns the full path to the module which is invoking the macro — i.e. the full path to each of our view modules. We use part of that path to build the relative template path that Phoenix.View accepts as an argument. We also need to pass those arguments down to Phoenix.View, so we change our view function as:

While I’m not using them, I provided default options to def view as an example of how to make things easier to transition in a larger project. If you adopt this you may want to use an alternate name from :view, so that you can have some views in the old structure and some in the new. For example you could make the new defmacro clause key on :cview, then in your new style views you use BigappWeb, :cview and in your old ones you still use :view. Only :cview would pass opts to the view function in _web.ex, so the default options (which match the generated Phoenix defaults) would be used for :view.

Finally, it will make sense to change your :live_reload config in config/dev.exs so you are reloading all your web folders, the below lines will do that in this example:

~r{lib/bigapp_web/.*(ex)$},
~r{lib/bigapp_web/.*(eex)$}

And that’s it! The example repository is on Github and you can see the exact changes required to move to this structure in this commit:

--

--