Subscribe to access all episodes. View plans →
Published October 11, 2017
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.
Blaine Gunn
7 years agonice
Alekx
7 years agoThanks!
Daryll Santos
6 years agoHi Alekx, cool video. As an Elixir and Phoenix noob (3 days in), what’s the use case for Module Plugs? Are these like for shared functionality between controllers? (Coming from Ruby so I’m sort of thinking this is a mixin)
Alekx
6 years agoHey Daryll, that’s definitely one way to use Plug, but Plug is really more of a way to receive and alter our connection.
When we receive a connection, Phoenix is essentially sending it through a series of Plugs and manipulating it as we go. In fact, the controller action is a function plug itself.