2009年8月8日土曜日

[Rails][プラグイン]acts_asプラグインの作りかた - 超入門 -

ブログなどについている、月別アーカイブ作成支援に汎用モジュールがほしかったので、(勉強も兼ねて)簡単なプラグインを作成しました。
(スキルレベル:知識ゼロ。プラグインてなに?)

少し調べてみたところ、ActiveRecordを拡張する acts_as_xxx という手法(フレームワーク?)が適当そうだったため、その作法に則ります。テーブルやカラムの追加等、あまり複雑なことはせず、ActiveRecord作成日付フィールド(普通は created_at)だけを参照します。

参考にしたのは、↓の記事と acts_as_taggable_on_steroidsプラグインのソース。

プラグイン作成

まずは、pluginをgenerateします。プラグイン名は acts_as_archivable。
$ script/generate plugin acts_as_archivable
generate を実行すると、vendor/plugin 以下に acts_as_archivable ディレクトリ(と中にプラグインの雛形)が作られます。
generateされたファイルのうち、編集したのは以下の2つだけです。
  • init.rb
  • lib/acts_as_archivable.rb
init.rb には次のコードを追加します。

# init.rb
require 'acts_as_archivable'

ActiveRecord::Base.class_eval do
include ActiveRecord::Acts::Archivable
end


init.rb は Railsによりアプリケーション初期化時に読み込まれます(西和則著「Ruby on Rails入門―優しいRailsの育て方」)。ここでは、ActiveRecord::Baseクラスに、ActiveRecord::Acts::Archivable モジュールをMix-inしているだけです。

Archivableモジュールの実体は、lib/acts_as_archivable.rb の中で定義します。

# acts_as_archivable.rb
module ActiveRecord
module Acts
module Archivable
def self.included(base)
base.extend(ActMacro)
end
end

module ActMacro
# acts_as_ メソッド(ActiveRecordクラス内で呼び出す)
# 例外処理、クラスメソッドのインクルード
def acts_as_archivable(col_name = "created_at")
# 指定された作成日カラムがActiveRecordクラスになければ例外を発生させる
raise NoCreatedAtColumnError unless self.column_names.include? col_name
# 指定された作成日カラムの型が:datetimeでなければ例外を発生させる
raise TypeError unless self.columns_hash[col_name].type.equal? :datetime
# クラスメソッドをインクルード
self.extend(ClassMethods)
self.set_created_col_name col_name
end

class NoCreatedAtColumnError < StandardError; end
class TypeError < StandardError; end
end

module ClassMethods
# ActiveRecordと、作成日カラム名を紐付けるマップ
@@class_col_hash = Hash.new
def set_created_col_name col_name
@@class_col_hash.store self.class.name, col_name
end
def get_created_col_name
@@class_col_hash[self.class.name]
end

# 月別アーカイブ作成支援メソッド
# ここでは、月ごとに作成されたActiveRecordの件数をもつハッシュを作成して返している
# ex. {2008=>{5=>10,8=>3,11=>6},2009=>{1=>5,3=>5,...}}
# パフォーマンス/使い易さを考えると、もう少しやりようがあると思うけどとりあえず。。。
def monthly_archive_counts
counts = Hash.new
records = self.find(:all)
records.each do |record|
year = record[get_created_col_name].year
month = record[get_created_col_name].month
if counts[year] == nil
counts.store year, Hash.new(0)
end
counts[year][month] += 1
end
return counts
end
end
end
end


self.includedというのはModuleクラスのメソッドで、リファレンスマニュアルの説明には
self が include されたときに対象のクラスまたはモジュールを引数にインタプリタから呼び出されます。

とあります。

acts_as_archivableが、(プラグインに慣れている人にはたぶんおなじみの)、拡張対象のActiveRecordクラスから呼び出すメソッドになります。
作成日カラムとして"created_at"以外を使う場合に対応できるよう、作成日カラム名を与えるようにしています(デフォルト値は"created_at")。
  • 指定された作成日カラムが存在しない場合
  • 存在するが :datetime タイプでない場合
には、例外を発生させています。
例外処理のあと、ActiveRecordと作成日カラム名の対応づけと、目的となるクラスメソッドのインクルードを行います。

使いかた

準備として

class Bookmark < ActiveRecord::Base
acts_as_archivable
end

のように ActiveRecord クラス内で acts_as_archivable メソッドを呼ぶだけです。これで、Bookmarkクラスに monthly_archive_counts メソッドが追加されます。

bookmarksテーブルに
sqlite> select id, created_at from bookmarks;
5|2008-10-01 09:00:00
6|2008-10-01 09:00:00
7|2008-10-01 09:00:00
8|2008-11-01 09:00:00
9|2009-02-01 09:00:00
10|2009-02-01 09:00:00
11|2009-04-01 09:00:00
12|2009-04-01 09:00:00
13|2009-04-01 09:00:00
14|2009-04-01 09:00:00
16|2009-06-01 09:00:00

というデータが入っているとき、Bookmark.monthly_archive_counts は次のハッシュを返します。

$ script/console
Loading development environment (Rails 2.3.3)
>> counts = Bookmark.monthly_archive_counts
=> {2008=>{10=>3, 11=>1}, 2009=>{2=>2, 4=>4, 6=>1}}
>> counts[2009][4]
=> 4


ビューでの使用イメージはこんな感じ。


ちゃんと使えるプラグインを作ろうとすると、テーブルやカラムを追加したり色々複雑になりますが、acts_asプラグインの骨格としてはこんな感じでしょうか。

よくできてるなー。
Mix-inの強力さを(あらためて)実感。

0 件のコメント:

コメントを投稿