You’ve made a Phoenix controller before, but do you know how it actually works? Let’s explore some code together.
Make a new project
First, we’re going to make a new phoenix project. I’m including the --no-ecto
flag because I don’t plan on using any database or changeset functionality in this post.
mix phx.new controller_dissection --no-ecto
All the code you need to see will be included in the text of this post. But feel free to follow along by making the project on your machine or by following along with this GitHub repo.
The new project should look something like this (not every file is shown in this tree):
.
├── README.md
├── _build
├── assets
├── config
├── deps
├── lib
│ ├── controller_dissection
│ │ ├── application.ex
│ │ └── mailer.ex
│ ├── controller_dissection.ex
│ ├── controller_dissection_web
│ │ ├── controllers
│ │ │ └── page_controller.ex
│ │ ├── endpoint.ex
│ │ ├── gettext.ex
│ │ ├── router.ex
│ │ ├── telemetry.ex
│ │ ├── templates
│ │ │ ├── layout
│ │ │ │ ├── app.html.heex
│ │ │ │ ├── live.html.heex
│ │ │ │ └── root.html.heex
│ │ │ └── page
│ │ │ └── index.html.heex
│ │ └── views
│ │ ├── error_helpers.ex
│ │ ├── error_view.ex
│ │ ├── layout_view.ex
│ │ └── page_view.ex
│ └── controller_dissection_web.ex
├── mix.exs
├── mix.lock
├── priv
└── test
We haven’t done anything special yet, this is just the default structure for a Phoenix project.
Down the rabbit hole
In this post we’re interested in the controllers. To find them, we need to look in lib/controller_dissectino_web/controllers
You’ll only have one controller by default, and it’s in page_controller.ex
. Here’s what it should look like:
defmodule ControllerDissectionWeb.PageController do
use ControllerDissectionWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end
This line near the top should look pretty familiar if you’ve made a Phoenix controller before:
use ControllerDissectionWeb, :controller
But what does it actually do?
use/2
is a macro that calls the __using__
macro for the given module. Here, the module is ControllerDissectionWeb
.
If we open up lib/controller_dissection_web.ex
then we’ll see the following __using__
macro defined near the bottom of the file:
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
The __MODULE__
special form provides the atom for the current module, and the which
parameter would be :controller
, because that’s what’s being passed in with the use
call in page_controller.ex
.
So all that happens with this use statement is that ControllerDissectionWeb.controller/0
is called.
If we take a look at ControllerDissectionWeb.controller/0
we’ll see the real meat of the Phoenix controller:
def controller do
quote do
use Phoenix.Controller, namespace: ControllerDissectionWeb
import Plug.Conn
import ControllerDissectionWeb.Gettext
alias ControllerDissectionWeb.Router.Helpers, as: Routes
end
end
This function is a macro that will inject code into modules such that they will be able to carry out the defined behavior of Phoenix controllers.
While I won’t get too into the weeds in this post, I encourage you to follow the definition of Phoenix.Controller
to see the Phoenix functions that get imported into your controllers.
Injecting our own code into controllers
To test that this code is actually injected into our controllers, we can import a module here and see if we can access it within a controller. It’s generally better to put new modules in a new file, but for simplicity let’s just put it in lib/controller_dissection_web.ex
.
Within this new module, we’ll make a simple hello world function:
defmodule HelloWorld do
def hello_world do
IO.puts("hello, world!")
end
end
defmodule ControllerDissectionWeb do
# ...
end
Now, let’s add an import to the HelloWorld
module inside the controller/0
macro:
def controller do
quote do
use Phoenix.Controller, namespace: ControllerDissectionWeb
import Plug.Conn
import ControllerDissectionWeb.Gettext
alias ControllerDissectionWeb.Router.Helpers, as: Routes
import HelloWorld
end
end
To test out that it’s available, navigate back to lib/controller_dissection_web/controllers/page_controller.ex
and try calling hello_world/0
within the index/2
function. If successful, the string "hello, world!"
will be printed whenever the index page is loaded.
defmodule ControllerDissectionWeb.PageController do
use ControllerDissectionWeb, :controller
def index(conn, _params) do
hello_world()
render(conn, "index.html")
end
end
Now that everything is in place, we’ll run the server with mix phx.server
and open up localhost:4000
If all goes well, you should see this in the console:
[info] GET /
hello, world!
What else can we do?
So now that you know how this works, what can you do with this knowledge?
Here’s a few things that come to mind:
- common utils library
- action handlers that you want to be present on every controller
- make a more specific type of controller that calls
controller/0
but also has additional functionality
Just for fun, let’s see if we can do a trivial implementation of that last option.
Extending controller functionality
Luckily, for making our own custom type of controller, there’s some existing code in controller_dissection_web.ex
that we can use as a reference.
The view_helpers/0
function defines some common functionality that is used in different types of views:
def view do
quote do
use Phoenix.View,
root: "lib/controller_dissection_web/templates",
namespace: ControllerDissectionWeb
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
# Include shared imports and aliases for views
unquote(view_helpers())
end
end
def live_view do
quote do
use Phoenix.LiveView,
layout: {ControllerDissectionWeb.LayoutView, "live.html"}
unquote(view_helpers())
end
end
# ...
defp view_helpers do
quote do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
import ControllerDissectionWeb.ErrorHelpers
import ControllerDissectionWeb.Gettext
alias ControllerDissectionWeb.Router.Helpers, as: Routes
end
end
The view/0
and live_view/0
functions both provide macros that nest the view_helpers/0
macro within them, allowing for multiple types of views that have some shared functionality. If you take a look around that file, you can find a few other functions that also call view_helpers/0
.
All that’s needed to make this work is to pass the nested macro into unquote/1
and follow it up with whatever else you want to include in the macro.
So we can treat controller/0
as the shared code for all controllers, and then make another macro function with a call to controller/0
.
First, we need to make a new module with the functionality for the new controller type.
defmodule CustomControllerUtils do
def flash_hello_world(conn, _opts) do
conn
|> Phoenix.Controller.put_flash(:info, "hello, world!")
end
end
Then, we can make the macro function for our custom controller type. All that’s special about our custom controller is that we’ll have a plug that puts a flash message of “hello, world!” This particular functionality is arbitrary and is only here to demonstrate that you could insert any code you like.
def custom_controller do
quote do
unquote(controller())
import CustomControllerUtils
plug :flash_hello_world
end
end
How would you use this?
Have you ever made changes to the Phoenix controller macros? Did this post help you think of a good use case? I’d love to hear about it. Let me know in the comments!
Top comments (1)
Have you ever made changes to the Phoenix macros?
Have you ever found a situation where macros were helpful in a Phoenix or Elixir project?