views:

1679

answers:

3

How should one have several different controller' actions set a common instance variable for use in templates but after the action runs.

In other words, I want this to work in my application_controller.

class ApplicationController < ActionController::Base
  after_filter :set_something_common

  def set_something_common
    # All controllers' actions have queried the DB and set @foo for me...
    @bar = some_calculation_on(@foo)
    # ... and all templates expect @bar to bet set.
  end
end

This does not work because after_filter runs after rendering. Fine. But what is the correct pattern?

Again, it is important that set_something_common runs after the action because those actions do case-specific things; but they all set @foo.

None of my ideas seem ideal:

  • Call set_something_common() towards the bottom of every action that needs it.
  • Refactor all controllers' case-specific code into case_specific_code() and force them to run in order:

    before_filter :case_specific_code, :set_something_common
    
  • Subclass application_controller and redefine the index method.

Any thoughts? Thanks.

Edit: Matthew's response prompted me to clarify:

Several controlers' index() all do pagination, each taking parameters @offset and @limit (via a global before_filter) to view data slices. Great. Now I want a common method to compute a RESTful URL for the "next slice" link. I was encouraged to see that url_for() generates a URL returning to the same resource, so I tried:

def set_something_common # really called set_next_url, truth be told
  @next_url = url_for(:offset => @offset + @limit, :limit => @limit)
end

I will try monkey patching Fixnum, so I can do something like @offset.next_url_for(self, @limit) from the template, but I'm not sure if it will work. Come to think of it, if I am going to modify the templates, then I may as well set up an application helper. I'm still not sure what the best solution is.

Update: Accepted answer is "use a helper."

Thanks for the updates from everybody. I learned my lesson that helpers, like global variables, are there for a reason and not to be eschewed when they are plainly beneficial and succinct.

+2  A: 

I would have a method on @foo which returns a bar, that way you can use @foo.bar in your views.

<% @bar = @foo.bar %> #if you really don't want to change your views, but you didn't hear this from me :)

MatthewFord
Thanks, Matthew. That is a great idea. I didn't think of it since @foo is an integer and I need to compute a RESTful URL, but hey I don't see why not!I will give this some consideration and update the question. I may bounty this answer and that puts you in top contention!
jhs
+2  A: 

Firstly, you don't want to try to insert code "between" a controller action and a template rendering. Why? Because you want the controller action to have the freedom to choose what sort of response to give. It could return XML, JSON, headers only, a redirection, nothing, etc. That's why after filters are executed after the response has been rendered.

Secondly, you don't want to monkey patch Fixnum. I mean, maybe you do, but I don't. Not often at least, and not unless I get some totally wicked semantic benefits from it, like being able to say 3.blind_mice. Monkey patching it for a random use case like this seems like a maintenance headache down the road.

You mention refactoring out all the controllers' case specific code into a before filter and running them sequentially. Which brings up to my mind... @foo is the same in every case? If that's the case, then one before filter would work just fine:

before_filter :do_common_stuff
def do_common_stuff
  @foo = common_foo
  @bar = do_something_with @foo
end

That's a totally legit approach. But if @foo changes from controller to controller... well, you have a few more options.

You can separate your before filters into two halves, and customize one per controller.

# application_controller:
before_filter :get_foo, :do_something_common
def do_something_common
  @bar = do_something_with @foo
end

# baz_controller:
def get_foo
  @foo = pull_from_mouth
end

#baf_controller:
def get_foo
  @foo = pull_from_ear
end

But you know, if it's a simple case that doesn't need database access or network access or anything like that... which your case doesn't... don't kill yourself. And don't sweat it. Throw it in a helper. That's what they're there for, to help. You're basically just rearranging some view data into a form slightly easier to use anyway. A helper is my vote. And you can just name it next_url. :)

Ian Terrell
Thanks for the thorough feedback, Ian. As you can see from my updated situation, my specific case is that each controller's index method sets @offset and @limit, and I want a common method to come in behind that and set @next_url so my templates can use all three. So you would probably agree that it's awkward to move very typical index method code into a before filter just to fit the API. Thoughts?
jhs
Using a before_filter in ApplicationController for that is only awkward to the extent that without any further customization it will apply to every action in every controller. I prefer to be more scalpeled. Using a helper method (lowercase h, protected ApplicationController method) that gets called everywhere needed is actually not terrible either. Not everything has to be magic, calling methods like that has been done for years. Using a Helper method (uppercase H, app/helpers/application_helper.rb) to generate the URL is also a prime candidate since it's actually just view information.
Ian Terrell
Thanks again for your response, Ian. I did in fact go with the lower-case-h helper solution, with a plan to refactor based on what I learn here.I agree: not everything need be magic; however I thought it's a common pattern to repeat the same task *after* several actions but *before* rendering. I read elsewhere that "filter" is the opportune word, and it's bad form to use filters as generic pre-processors. That's reasonable.I am also considering wrapping either the index() method or perhaps render().
jhs
Final thought. I re-read your suggestion about just an orthodox helper, and that may be a good fit. I just need to confirm whether url_for() with no argument returns the RESTful URL for the current resource, which is what happens when I call it from the controller. If so, then next_url could just return url_for(:next => @next + @skip, :skip => @skip)
jhs
Glad you're getting some food for thought. As far as how common the pattern is, no other cases stand out in my memory in over 3 years of Rails development where identical code needed to be run after an action but before rendering. It's always been the case that all of the code that was shared could fit in a before filter. Even if it has to access the model it's managing (which will be different per controller), if you follow Rails naming conventions you can always do something like controller_name.singularize.camelize.constantize to access it. :)
Ian Terrell
+2  A: 

Use <%= do_some_calculations(@foo) %> inside your templates. That is the straight way.

Bogdan Gusiev