cookieを保持、参照するplugin

クッキーの値をDBに格納しておき(Visitorというモデル名にする)、
次回同じユーザがアクセスしたときに格納しておいた情報を参照できるようにするというもののPluginを作ってみました。

マイグレーション

まず,cookieを保存しておくテーブルを生成。PluginのRakeで生成できるよう、railsのconfigの接続設定を使ったテーブル生成を行う。

require "active_record"

class MigrateVisitors
  
  def self.create
    self.establish_connection
    MigrateVisitors::CreateVisitors.up
  end

  def self.drop
    self.establish_connection
    MigrateVisitors::CreateVisitors.down
  end

  private 
  
  def self.establish_connection
    conf = YAML.load_file(File.dirname(__FILE__) +  "/../../../../config/database.yml")
    env = if ENV['RAILS_ENV'] then ENV['RAILS_ENV'] else 'development' end
    ActiveRecord::Base.establish_connection(conf[env])
  end


  class CreateVisitors < ActiveRecord::Migration
    def self.up
      create_table :visitors do |t|
        t.column :value, :string, :limit => 32, :null => false
        t.column :hashed, :string, :limit => 32, :null => false
        t.column :expired, :date, :null => false
        t.timestamps
      end
    end

    def self.down
      drop_table :visitors
    end
  end
end

カラムとして以下のものを定義しています。

cookieの値
上記の値と現在日時を元にMD5でハッシュ化した値を入れておくカラム、この値もcookieとしてクライアントに格納して、cookieの値の送信者が正当であるかを照合する。
cookieの有効期限

マイグレーションタスクを定義したRakeは以下のとおり。

require 'rake'


task :up do
  require File.dirname(__FILE__) + '/lib/migrate_visitor'
  require File.dirname(__FILE__) + '/lib/visitor'
  MigrateVisitors.create
end

task :down do
  require File.dirname(__FILE__) + '/lib/migrate_visitor'
  require File.dirname(__FILE__) + '/lib/visitor'
  MigrateVisitors.drop
end

テーブル生成は、たとえば以下のようにして行う(development環境での例)

rake up ENV=development

Model

lib内にcookie情報のテーブルモデルvisitorを定義。
以下のようにcookie情報を格納するのvisitorモデルを定義。cookieの取得、生成を行うfind_from_cookieメソッドを定義しました。

require "active_record"

class Visitor < ActiveRecord::Base
# サイト訪問者のcookieを記録するモデル
# +value+(クッキーの値), +hashed+(照合用のハッシュ値のクッキー値), +expire+を含むモデル
  attr_accessor :must_send

  public
  # cookie情報をもとに行を検索
  # 未作成または、期限まで1ヶ月以内であれば新規生成
  # 有効期限が近ければ更新
  def self.find_from_cookies(cookies)
    visitor = if cookies[:visitor] then
      value = cookies[:visitor]
      Visitor.find_by_value(value)
    else
      nil
    end
    new_visitor = if !visitor or visitor.hashed !=  cookies[:visitor_md] then  
      # 新規(未作成 or ハッシュとの不一致)
      Visitor.new
    elsif visitor.expired < Date.today + 30 then 
      # 更新(30日以内に有効期限切れ)
      visitor
    else
      # 変更不要
      nil
    end
    if new_visitor 
      value = create_cookie_value
      new_visitor.value = value
      new_visitor.hashed = Digest::MD5.hexdigest(value+Time::now.strftime("%Y%m%d%H$M$s"))
      new_visitor.expired = 3.month.from_now
      new_visitor.must_send = true
      new_visitor.save
      new_visitor
    else
      visitor.must_send = false
      visitor
    end
  end
  
  private
  
  # クッキーの値を生成
  def self.create_cookie_value()
    ActiveSupport::SecureRandom.hex(16)

  end
end

cookieの有効期限が近いか、cookieがなければcookieを新規作成します。
cookieはランダム値のcookieとそのランダム値と日時を結合してMD5ハッシュ化したものを生成してDBへの保存します。クライアントからの送信時、その2つのcookie値を照合して正当性を判断します。

includeされるモジュール

controllerにincludeされるためのモジュールを以下のように定義

require "visitor"
module ActVisitor
  
  attr_reader :visitor
  
  def self.included(controller)
    controller.before_filter :set_visitor_cookie
  end
  
  # Visitorのcookie生成
  # 未作成または、期限まで1ヶ月以内であれば新規cookieを生成
  def set_visitor_cookie()
    visitor = Visitor.find_from_cookies(cookies)
    if visitor.must_send :
      cookies[:visitor] =
        {:value => visitor.value,
          :expires => visitor.expired
        }
      cookies[:visitor_md] =
        {:value => visitor.hashed,
          :expires => visitor.expired
        }
    end
    @visitor = visitor
  end
  
end
visitorモデルのfind_from_cookieメソッドでモデルのインスタンスを取得。それをcookieとして送信するメソッドを定義。
モジュールがincludeされた時に呼び出されるincludedメソッドをオーバーライド。include元のbefore_filter(前処理)として、上記のcookie送信メソッドが自動的に呼ばれるようにする。

controllerからの使用

各コントローラのベースとなるコントローラーで上記のように作成したActVisitorモジュールをincludeします。
これで,これを継承したコントローラーからvisitorメソッドによりモデルのインスタンスを参照できるようになります。

class Customer::BaseController < ApplicationController
  
    include ActVisitor

end

使用例

今回は、cartの情報を保持するために利用しました。


以下は、テーブル定義。visitorモデルへのidを含んでいます。

  create_table "carts", :force => true do |t|
    t.integer  "customer_id"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.integer  "visitor_id"
  end

以下はCartのモデル。visitorモデルから、cartを取得するメソッドを定義しています。
class Cart < ActiveRecord::Base
  has_many :cart_items
  
  #
  # カートを返す
  # 指定したvisitorモデルのidを保持するカートを返す
  # なければ、nilを返す
  #
  def self.find_from_visitor(visitor)
    if visitor and visitor.id then
      self.find_by_visitor_id(visitor.id)
    end
  end

...
...

end

コントローラー側の一覧表示アクションは、簡単な例では、たとえば以下のようになります。

class Customer::CartController < Customer::BaseController

  # カート一覧の表示
  def list
    @cart = Cart.find_or_create_from_visitor(visitor)
  end

end
includeされているvisitorメソッドより、visitorモデルを取得
Cartモデルで定義したメソッドにより,visitorに対応するcartモデルのインスタンスを得る。