How to Make Session Data Available to Models in Ruby on Rails?

How to Make Session Data Available to Models in Ruby on Rails?

Ruby on Rails is implemented as the Model View Controller (MVC) pattern. This pattern separates the context of the Web Application (in the Controller and the View) from the core Model of the application. The Model contains the Domain objects which encapsulate business logic, data retrieval, etc. The View displays information to the user and allows them to provide input to the application. The Controller handles the interactions between the View and the Model.

This separation is a very good design principle that generally helps prevent spaghetti code. Sometimes though the separation might break down.

Rails provides the Active Record Callbacks which allows you to write code that will respond to the lifecycle events of the Model objects. For example you could log information every time a specific kind of Model object is saved. For example you could record some information every time an Account changed using the following:

account.rb
1
2
3
4
5
6
7
8
class Account < ActiveRecord::Base
  after_update :log_audit_change

  private
  def log_audit_change
    Audit.audit_change(self.id, self.new_balance)
  end
end

You might have noticed a limitation with the previous API though. You didn’t notice? The only information passed to the model is the Object that is being changed. What if you want more context than this? For example, what if you want to audit not only the values that changed them, but the user who made the change?

account.rb
1
2
3
4
5
6
7
8
class Account < ActiveRecord::Base
  after_update :log_audit_change

  private
  def log_audit_change
    Audit.audit_change(current_user, self.id, self.new_balance)
  end
end

How do you get the current_user value? Well, you have to plan ahead a little bit. The User in this application is stored in the HTTP Session when the user is authenticated. The session isn’t directly available to the Model level so you have to figure out a way around this. One way to accomplish this is by using a named Thread local variable. Each HTTP request is served by its own thread. That means that a variable stored as thread local will be available for the entire processing of a request.

The UserInfo module encapsulates reading and writing the User object from/to the Thread local. This module can then be mixed in with other objects for easy access.

user_info.rb
1
2
3
4
5
6
7
8
9
module UserInfo
  def current_user
    Thread.current[:user]
  end

  def self.current_user=(user)
    Thread.current[:user] = user
  end
end

A before_filter set in the ApplicationController will be called before any action is called in any controller. You can take advantage of this to copy a value out of the HTTP session and set it in the Thread local:

user_info.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ApplicationController < ActionController::Base
  before_filter :set_user

  protected
  def authenticate
    unless session[:user]
      redirect_to :controller => "login"
      return false
    end
  end

  # Sets the current user into a named Thread location so that it can be accessed by models and observers
  def set_user
    UserInfo.current_user = session[:user]
  end
end

At any point in a Model class that you need to have access to those values you can just mixin the helper module and then use its methods to access the data. In this final example we mixin the UserInfo module to our model and it will now have access to the current_user method:

account.rb
1
2
3
4
5
6
7
8
9
10
class Account < ActiveRecord::Base
  include UserInfo

  after_update :log_audit_change

  private
  def log_audit_change
    Audit.audit_change(current_user, self.id, self.new_balance)
  end
end

You generally shouldn’t need this kind of trick outside of a model. In most cases the Controller should pass all of the information needed by a Model object to it through its methods. That will allow the Model objects to interact and the Controller to do the orchestration needed. But in a few special cases, this trick might be handy.

So far so good, That’s it!!! See ya!!! :)