How to handle global Controller data?

I’m struggling with converting a Rails application to Phoenix. In the Rails app, I had a global ApplicationController which would retrieve the messages for the current user. The messages would be used by the layout which rendered a messages partial; this shows up on every page.

What’s the best and correct way to do this in Phoenix?

I have the layout rendering the _messages.html.eex partial. And the partial loops through the messages:

<$= for message <- @messages do %>

There doesn’t seem to be an ApplicationController, but I’ve tried creating a controllers/helpers.ex (MyApp.ControllerHelpers) which I wire-up in web.ex. I had to convert it to a function which accepts a conn and then pass the conn from the layout to the partial etc. That didn’t seem right, and didn’t really work. I moved it to a views/helpers.ex (MyApp.ViewHelpers). It returns an undefined messages function. I tried using the full module name like MyApp.ViewHelpers.messages, but that didn’t work either.

<$= for message <- MyApp.ViewHelpers.messages(@conn) do %>

I’m just trying to find out the best way to do this…

Thanks.

1 Like

In this case, because it is something that gets added to the current user, you probably want to put it in a Plug. (You can view Plugs somewhat of a combination of Rails’ middleware and before_action controller statements).

To make the plug, simply create a new file in a subdirectory of web (either in the web/controllers folder, or in a separate folder, for instance named 'web/plug).

It might contain something like:

defmodule YourApp.AddCurrentUserMessages do
  use Plug.Conn
  def call(conn, _opts) do
    messages = Repo.all(Message) # Swap this out for the actual way you are receiving User-specific messages.
    assign(conn, :messages, messages)
  end
end

Then add the following line to your router.ex, in the :browser pipeline (or, if it’s only a section of the website, you might want to add it just in that scope): plug YourApp.AddCurrentUserMessages

Now, you can refer to messages like @messages from all your views.

4 Likes

Basically:

  • stuff for a single action? -> Put it in that action function.
  • stuff for multiple actions? -> Put it in a plug, and refer to it in the controller. Examples: plug :put_layout, "controller_specific_layout.html" or plug :scrub_params, "posts" when action in [:create, :update]
  • stuff for multiple controllers? -> Put it in a plug, and refer to it in the appropriate pipeline or scope in your Router.
8 Likes

@Qqwy That’s a great explanation! Thanks!

1 Like

It is a very good explanation, indeed. What I wonder about though is a scenario, where – let’s say – I have a plug, referred to in the pipeline, which means it will be invoked on every controller and every action, right? Now, there’s one or two actions in the whole application, for which I want to skip the plug. In Rails I would put a before_action in ApplicationController and then a skip_before_action in the controller with actions I want to disable the extra step for. A real world example would be:

skip_before_action :authenticate, only: [:new, :create]

in class SessionsController < ApplicationController, which is the only one allowed to run its two actions for unauthenticated user. Obviously, because otherwise the user would never be able to authenticate. Now, what would be the “right” Phoenix approach to this? Is there an equivalent of skip_before_action? Or is another approach preferable?

2 Likes