Stephen Celis
Rails controllers, views, and variables 6 Sep
In object-oriented programming, an object uses instance variables to store private information. If you want to access an object’s instance variables, those variables should only be accessible through instance methods. At least, this is the expectation.
So when one dives into Rails, they may find it quite confusing when an instance variable set in a controller is also available in the view. Convenient, sure…but it somehow doesn’t feel true to Ruby. The controller and view are different objects with their own scopes, right?
How hard is it to be a purist? How can we tell ActionController::Base to be a little more discreet?
# config/initializers/less_gossip.rb
# Stop sharing so much!
ActionController::Base.class_eval { def add_instance_variables_to_assigns; end }
# Stop caring so much!
ActionView::Base.class_eval { def assign_variables_from_controller; end }
Now the controller has some privacy. Rails won’t force it to tell all its secrets to the view. The controller still needs to do its job, though, which likely involves retrieving data and serving it to the view. So how to we mediate the flow of information without instance variables? The same way Ruby usually does: readers.
Let’s say we have a CRUD controller for articles. ArticlesController#index would normally assign @articles, so let’s define a reader method, articles, instead.
# app/controllers/articles_controller.rb
def articles
Article.all
end
Now we can call articles throughout the controller, and it will return what @articles previously would have. Rails is pretty good about caching database queries, but that doesn’t mean we shouldn’t memoize as a best practice for optimization:
def articles
@articles ||= Article.all
end
Ah, instance variables used as they were meant to be used.
So what about @article? Doesn’t its value change depending on the action being called? We just have to be a little smarter. Several lines of @article = Article.find(params[:id]) wasn’t very DRY, anyhow.
def article
@article ||= if params[:id]
Article.find params[:id]
else
Article.new params[:article]
end
end
There we go. We’re accessing variables through getters! When we’re in the view, we only need to call those methods on the controller, which is available through ActionView::Base#controller. Here’s an example:
# app/views/articles/index.html.erb
<%= render :partial => controller.articles %>
What? That’s pretty ugly? And what about the controller public instance methods? They should refer to actions?
I guess we can’t completely ignore Rails’ conventions. We’ll have to make those getters private. But how, now, can we deliver these methods to the view? Luckily, Rails makes this easy with helpers:
# app/controllers/articles_controller.rb
helper_method :articles, :article
Now it looks nicer in the view, if a bit plainer than the @ decorations we’re all used to:
# app/views/articles/index.html.erb
<%= render :partial => articles %>
There are, actually, a few benefits to all this maneuvering (beyond clarity for the classically-trained Ruby programmer):
- Maintainability: any
before_filteryou used, in the past, to set instance variables, is now irrelevant. You no longer have to worry about scoping it with:onlyor:except, or updating it when you add, remove, or rename actions. - Cacheability: these methods will only run when they’re first called during a request. If they only appear in the view, and the view is cached, you won’t hit the database.
Here’s our completed controller code:
class ArticlesController < ActionController::Base
helper_method :articles, :article
# render index, show, new, edit
def create
article.save!
redirect_to articles_path
end
def update
article.update_attributes! params[:article]
redirect_to articles_path
end
def destroy
article.destroy
end
private
def articles
@articles ||= Article.all
end
def article
@article ||= if params[:id]
Article.find params[:id]
else
Article.new params[:article]
end
end
end
September 9 update
Mike Burrows points out that if ActiveRecord::RecordNotFound is raised in the template, you are given a 500 error in production. ActionController::Rescue normally handles the 404s, while ActionView wraps exceptions in its own TemplateError (always 500), so—coupling aside—we’ll just have to make the view more aware:
# config/initializers/less_gossip.rb
class ActionView::Template
def render_template(view, local_assigns = {})
render(view, local_assigns)
rescue Exception => e
raise e unless filename
case e
when ActiveRecord::RecordNotFound # Ah. Here we are.
raise e
when TemplateError
e.sub_template_of(filename)
raise e
else
raise TemplateError.new(self, view.assigns, e)
end
end
end
Or, if you’re not on the edge (i.e., you’re running 2.1.1 or—heaven forbid—less):
# config/initializers/less_gossip.rb
class ActionView::Template
def render_template
render
rescue Exception => e
raise e unless filename
case e
when ActiveRecord::RecordNotFound
raise e
when TemplateError
e.sub_template_of(filename)
raise e
else
raise TemplateError.new(self, @view.assigns, e)
end
end
end