#20: Intro to Module Plugs

Elixir 1.4.5

Phoenix 1.3.0

Plug 1.4.3

View source on GitHub


Let’s pick up where we left off in episode 19 by converting our function plug to a module plug.

Back in our MovieController, we have our function plug movie_total that assigns the total number of our movies to our connection.

However, we don’t need it to be set for every action in our resource. Let’s update it to only be set for the index action.

after plug :movie_total let’s add the guard clause: when action in [:index]

lib/teacher_web/controllers/movie_controller.ex

defmodule TeacherWeb.MovieController do
  plug :movie_total when action in [:index]
  …
end

Then let’s the open our app.html.eex and remove the line where we render the movie total.

Now in our movie index.html.eex template we can add our movie total like we had before

Template path: lib/teacher_web/templates/movie/index.html.eex

...
<p>Movie total: <%= @conn.assigns[:movie_total] %></p>
...

Another way we can access our movie_total is with at movie total. And since we always expect our movie total to always available for this action, let’s use it instead.

Template path: lib/teacher_web/templates/movie/index.html.eex

...
<p>Movie total: <%= @movie_total %></p>
...

Let’s jump to the browser and ensure we only see the movie total on our index action - and great it’s still working as expected.

Now let’s open our MovieData module and convert it to a module plug.

A module plug has two functions: init and call.

init initializes a set of options.

We’ll just return our options to start.

call takes a Plug.Conn struct and the options returned by our init function. This is where we would modify our struct. It also needs to return our connection struct.

Let’s use the same logic from our movie_total function for now.

lib/teacher/movie_data.ex

defmodule Teacher.MovieData do
  …
  def init(opts) do
    opts
  end

  def call(conn, _opts) do
    movie_total = Repo.one(from m in Movie, select: count("*"))
    assign(conn, :movie_total, movie_total)
  end
end

Now that we have our module plug, let’s go back to our MovieController and update it to use our new plug.

First we’ll want alias our new module, so let’s change our import to alias.

Then we’ll replace our function plug name with the our module plug, MovieData.

lib/teacher_web/controllers/movie_controller.ex

defmodule TeacherWeb.MovieController do
  use Teacher.Web, :controller
  alias Teacher.MovieData
  alias TeacherWeb.Movie
  
  plug MovieData when action in [:index]
  …
end

Back in our browser we see that our module plug is working - our movie total is being loaded.

Now let’s go back to our module plug, and in the init function we’re just returning any options that are passed in. We’re then ignoring them in the call function. Let’s change that.

Let’s pass in a message as an option that we’ll render along with our movie total.

The options passed into our init function are a keyword list, so let’s use Keyword.fetch/2 to return our message.

Now we could use Keyword.fetch!/2, which will return our message or raise an exception, whereas the Keyword.fetch/2 function, will return an :ok tuple with our message or :error if they key doesn’t exist.

Let’s use this (and have some fun) using pattern matching to match against our two cases.

We’ll update our call function accept our message as the second argument.

Then we’ll get our movie_total at the top of our function.

Now we can use a case statement with our message.

First we’ll pattern match against our success case, which will return an ok tuple with our message.

Next we’ll pattern match against our error case.

Great now if we have a message, we’ll build a custom message that contains our fetched message along with our movie_total.

Then let’s assign it to our connection as movie_total_msg.

Then in our :error case, let’s create a default message, which will include our movie_total.

Again we’ll assign it to our connection as movie_total_msg

lib/teacher/movie_data.ex

defmodule Teacher.MovieData do
  …
  def init(opts) do
    Keyword.fetch(opts, :msg)
  end

  def call(conn, msg) do
    movie_total = Repo.one(from m in Movie, select: count("*"))

    case msg do
      {:ok, msg} ->
        custom_msg = "#{msg} #{movie_total}"
        assign(conn, :movie_total_msg, custom_msg)
      :error ->
        default_msg = "We found #{movie_total} movies."
        assign(conn, :movie_total_msg, default_msg)
    end
  end
end

Then let’s open our movie_controller and we’ll include a message as an option for our plug.

lib/teacher_web/controllers/movie_controller.ex

defmodule TeacherWeb.MovieController do
  …
  plug MovieData, [msg: "Your total number of movies:"] when action in [:index]
  …
end

With that we can then open our movie index.html.eex and update our message to just render @movie_total_msg

Template path: lib/teacher_web/templates/movie/index.html.eex

...
<p><%= @movie_total_msg %></p>
...

And if we refresh our browser we see our custom message.

Now let’s go back to our plug and change our init function to fetch a key that doesn’t exist. Keyword.fetch(opts, :foo)

lib/teacher/movie_data.ex

defmodule Teacher.MovieData do
  …
  def init(opts) do
    Keyword.fetch(opts, :foo)
  end

  def call(conn, msg) do
    movie_total = Repo.one(from m in Movie, select: count("*"))

    case msg do
      {:ok, msg} ->
        custom_msg = "#{msg} #{movie_total}"
        assign(conn, :movie_total_msg, custom_msg)
      :error ->
        default_msg = "We found #{movie_total} movies."
        assign(conn, :movie_total_msg, default_msg)
    end
  end
end

Then back in our browser, we see our default message being displayed.