Diving into the macros that make Phoenix controllers work
Tags: elixir, functional, phoenix, webdev
Published: 2022-03-16 14:31:12

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.

```elixir
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](https://github.com/brew-install-buzzwords/controller_dissection).

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:

```elixir
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:

```elixir
use ControllerDissectionWeb, :controller
```

But what does it actually do?

`use/2` is a [macro](https://hexdocs.pm/elixir/Kernel.html#use/2) 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:

```elixir
@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](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#__MODULE__/0) 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:

```elixir
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:

```elixir
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:

```elixir
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.

```elixir
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:

```elixir
[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:

```elixir
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. 

```elixir
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.

```elixir
def custom_controller do
  quote do
    unquote(controller())

    import CustomControllerUtils

    plug :flash_hello_world
  end
end
```

![Screenshot of hello world flash message](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnvmn23880bsmizzwiou.png)

## 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!