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?