RoR: instances variables within controller methods - ruby-on-rails

My question is about controller methods (possibly included from an outside class) that work with instance variables. I frequently use a before_filter in controllers to set up certain variables, e.g.:
class DocumentController < ApplicationController
before_filter :fetch_document
def action
#document.do_something
end
private
def fetch_document
#document = Document.find(params[:id])
end
end
I've been working on a project in which a few controllers will share some functionality, say, document editing. My first thought was to extract the relevant methods, and get them from application_controller.rb or a separate module. But then I noticed I was writing code that looks like this:
def fetch_document
#document = Document.find(params[:id])
end
def do_something_to_document
#document.do_something
end
This sets off warning bells: do_something_to_document is essentially assuming the existence of #document, rather than taking it as an argument. Is this, in your sage opinions, a bad coding practice? Or am I being paranoid?
Assuming it is an issue, I see two general approaches to deal with it:
Check for the instance var and bail unless it's set:
def do_something_to_document
raise "no doc!" unless #document
[...]
end
Call the action with the instance var as an argument:
def do_something_to_document(document)
[...]
end
2 looks better, because it hides the context of the calling object. But do_something_to_doc will only be called by controllers that have already set up #document, and taking #document as a method argument incurs the overhead of object creation. (Right?) 1 seems hackish, but should cover all of the cases.
I'm inclined to go with 1 (assuming I'm right about the performance issue), even though seeing a list of methods referencing mysterious instance vars gives me hives. Thoughts? Let me know if I can be more clear. (And of course, if this is answered somewhere I didn't see it, just point me in the right direction...)
Thanks,
-Erik

If you really need document in different controllers, I'd do something like this:
class ApplicationController < ActionController::Base
private
def document
#document ||= Document.find(params[:document_id])
end
end
class FooController < ApplicationController
before_filter :ensure_document, :only => [:foo]
def foo
document.do_something
end
private
# TODO: not sure if controller_name/action_name still exists
def ensure_document
raise "#{controller_name}##{action_name} needs a document" unless document
end
end

As #variable are session/instance variable you will get a Nil exception in do_something_to_document method.
The first code is fine, because before_filter will always load your #document.
I suggest you to write something like that
def fetch_document(doc_id)
#document ||= Document.find(doc_id)
end
def do_something_to_document
my_doc = fetch_document(params[:id])
end
where do_something_to_document is in the controller (if not, dont use params[:id], even if you know you can access this global, use another explicit parameter). The ||= thing, will asssure that you call the base only once by request.

Related

Set an instance variable app wide in Rails

In my Rails application I have a class that I want to initialize and then access it throughout my controllers. So the idea is that I set it via the application controller if it's not already been defined:
class ApplicationController < ActionController::Base
before_action :set_custom_class
# create an instance of customclass if doesn't exist
def set_custom_class
#custom_class ||= CustomClass.new
end
end
An example of the class:
class CustomClass
def initialize; end
def custom_method
#custom_method
end
def custom_method=(content)
#custom_method = content
end
end
If I then have a controller like:
class MyController < ApplicationController
def method_1
# set the custom_method value on my instance
#custom_class.custom_method('Some content')
# return the value I set above
#variable = #custom_class.custom_method
redirect_to :method_2
end
def method_2
# I should be able to retrieve the same value from that same instance
#variable = #custom_class.custom_method
end
end
What I'm finding is that when calling method_1 the #variable will return my content fine, but when calling method_2 AFTER method_1 (so the custom_method for the app wide #custom_class has been set) it's returning nil.
Why isn't the instance being retained? The #custom_class shouldn't be creating a new instance as it's already been set. So I can't understand why the value I have set gets lost when requesting it.
You witnessing such behaviour, because state of a controller is not preserved between requests. For example, imagine that current_user method sets #current_user for one request and returns the same user for another one.
Please, consider an option of using cookies or database for sharing state between requests.
Otherwise, a workaround would be setting a class variable of CustomClass, but I don't recommend to do it.
Looks like your before_action will re-instantiate the new object on every request. That means that since you aren't passing anything through to the class in Method2, it will come out as NULL.
Since you said app-wide, why not make it app-wide?
In config/application.rb,
module App
class Application < Rails::Application
def custom_class
#custom_class ||= CustomClass.new
end
end
end
in your application code,
Rails.application.custom_class

Access to class instance variable

How can I read a class variable? In my case I can't get value from variable.
class MediaContentsController < ApplicationController
#randomUsrId = rand(100)
def create
puts #randomUsrId
end
end
First of all, #randomUsrId refers to an instance variable, not a class variable. You can access it through an instance of the class, not direct on the class. For a class variable, you should use ##randomUsrId.
What you are actually looking for is attr_accessor :randomUsrId, through this, you can read it on an instance method, and even can set it through an instance of the class.
Here's how:
class MediaContentsController < ApplicationController
attr_accessor :randomUsrId
#randomUsrId = rand(100)
def create
puts #randomUsrId
end
end
But #randomUsrId = rand(100) won't set #randomUsrId to a random number, at least it is not the recommend way. You should use before_action here.
class MediaContentsController < ApplicationController
attr_accessor :randomUsrId
before_action :set_user_id_to_a_random_number, only: :create
def create
puts #randomUsrId
end
private
def set_user_id_to_a_random_number
#randomUsrId = rand(100)
end
end
Edit:
Each time you call the set_user_id_to_a_random_number function, it will generate a different number based on rand(100) and store it inside #randomUsrId. If that's what you do not want, and you want to persist the same value, you can do something like following:
def set_user_id_to_a_random_number
#randomUsrId = rand(100) unless defined? #randomUsrId
end
Edit:
What I have stated works only for one request, if you have multiple request, it won't work. As Ryan Bates says here:
An instance variable only sticks around for a single request, so using the technique described will only benefit you if you need to call a method multiple times per request.
That leaves you with two options if you want to store something between multiple requests. Either you can go with Databases, or you can use something called memcached.
You probably doing something wrong in your code, since this isn't how Rails controller logic should be usually implemented, but let's get down to your question anyway. As I mentioned it isn't class variable, it's instance variable in class scope, so in order to access it, you should first get it from that scope:
class MediaContentsController
#randomUsrId = rand(100)
def create
puts self.class.get_random_usr_id
end
def self.get_random_usr_id
#randomUsrId
end
end
MediaContentsController.new.create
# => 44

How can I create an instance variable to all instances of the class in ruby, using metaprogramming?

I'm new to ruby/rails world, and I'm facing and issue that appears to be something trivial, but it isn't being that easy for me.
What I want to do, is to write a simple helper method, to help keep the controllers clean. This method will receive a symbol/string, and create an instance variable with the camelized received string, which will evaluate to a constant/class.
My attempts resume themselves to this:
class ApplicationController < ActionController::Base
...
protected
def self.service(name)
instance_eval do
instance_variable_set("##{name.to_s.pluralize}", name.to_s.camelize.constantize)
end
end
And, in my controller:
class UserController < ApplicationController
service :user
But when I do this, it create the instance variables in UserController, not UserController.new. So, I know what is wrong, but I couldn't find how can I do this right.
Can anyone help me with that?
The trouble is that this method is in the class scope:
def self.service(name)
instance_eval do
instance_variable_set("##{name.to_s.pluralize}", name.to_s.camelize.constantize)
end
end
This means it'll be setting class level instance variables - or class variables (kinda).
If you want to follow this approach, you'll need to store these all in an array or similar at the class level, which can be read out and turned into instance variables when the class is instantiated.
However, I don't like to mess with the initialize method on controllers. It's not the way to do things, so could change the class behaviour, break things or become fragile when changing versions.
In your ApplicationController, something like:
class << self
def service(name)
#services ||= []
return #services += name.map(&:to_sym) if name.is_a?(Array)
#services << name.to_sym
end
def service_names
#services
end
end
before_action :services
def services
self.class.service_names.each do |name|
instance_eval do
instance_variable_set("##{name.to_s.pluralize}", name.to_s.camelize.constantize)
end
end
end
This is untested.

Global Controller Variables in Rails

I have a ChatController and an #user variable in it. On the main page I display #user.name. I also have destroy and create methods that work with ajax, so when I delete a message from my chat, #user becomes nil. To prevent problems from calling name on a nil object, I can add #user=User.find_by_id(:user_id) to every method. But this becomes tedious if I have many methods. Can I declare #user=User.find_by_id(:user_id) once and DRY up my code?
Yes, this is done in a before_filter (Documentation).
Something like:
class ChatController < ActionController::Base
before_filter :find_user
private
def find_user
#user ||= User.find_by_id(params[:user_id])
end
end
You may also consider using Inherited Resources which automates this for you.

will initialize break the layout settings in rails?

In one of the controller, I need a specific layout. I added layout at the beginning. It works well.
But if I add an initialize function for some controller-based variable. Rails seems just ignore the layout command.
Is anyone have same problem? How can I fix it?
class AdminsController < ApplicationController
layout "layout_admins"
def initialize
#Title = "Admins"
end
def index
....... some code here
end
end
initialize is used internally to Rails to, well, initialize a new instance of your controller so it can then serve requests on it. By defining this method in this particular manner, you are breaking Rails.
There is a way through! A light at the end of the tunnel. A pot of gold at the end of the rainbow:
def initialize
#title = "Admins"
super
end
See that little super call there? That'll call the superclass's initialize method, doing exactly what Rails would do otherwise. Now that we've covered how to do it your way, let's cover how to do it the "officially sanctioned" Rails way:
class AdminsController < ApplicationController
before_filter :set_title
# your actions go here
private
def set_title
#title = "Title"
end
end
Yes, it's a little more code but it'll result in less frustration by others who gaze upon your code. This is the conventional way of doing it and I strongly encourage following conventions rather than doing "magic".
EDIT: If you're using Rails 5 then you'll need to use before_action instead of before_filter.
I'm not sure exactly how layout works its magic, but I'm willing to bet it's in a yield block in the ActionController#initialize method. So your overriding of initialize would explain the problem.
Looks like you have too options here:
Close out your new definition with super to call the ActionController initialize which should use the layout defined in the class.
eg:
def initialize
#Title = "Admins"
super
end
Use a before filter to initialize your variables. This is the Rails Way of initializing values in a controller
class AdminsController < ApplicationController
layout "layout_admins"
before_filter :set_title
def set_title
#Title = "Admins"
end
def index
....... some code here
end
end

Resources