SyntaxHighlighter

2011年11月7日月曜日

Redmine プラグイン その2

Redmine プラグイン その2

昨日の続きです。

Redmine プラグインの雛形の作成

Redmine のプラグインを作成するには、以下のようなコマンドを実行します。 Redmine 1.2.1 では、まだ Rails 2.3.X 系統なので、ruby script/generateでプラグインを作成します。

1
ruby script/generate redmine_plugin sample_plugin

このコマンドにより、vendor/pluginsディレクトリの下にredmine_sample_pluginというディレクトリが作られ、プラグインの雛形が生成されます。

プラグインの仕組み

今回は、簡単のためにデータベースへの保存などは行わず、表示方法をカスタマイズしたり、機能を追加したりする場合について書きます。

init.rbからrequireする

この雛形に含まれるファイルで、最重要なものはinit.rbです。

このファイルは、Redmine が起動する際に自動で読み込まれます。

また、読み込まれる順序は、Redmine の Core ファイルが読み込まれた後です。

前回のオープンクラスの部分で書きましたが、この順序で読み込まれることが保証されているので、init.rbの中で Redmine 関係のクラス (例えばIssueクラス) を再定義することにより、自由にメソッドを追加、削除、上書きすることができます。

例えば、以下のように書いたファイルをlib以下に置き、init.rbからrequireすると、Issueクラスにopen?メソッドを追加することができます。 (ただし、後で書くように、普通はもう少し整理された定型の書き方があります。)

1
2
3
4
5
6
7
8
# vendor/plugins/redmine_sample_plugin/lib/test.rb
# init.rb で require される。
 
class Issue
  def open?
    return !(self.closed?)
  end
end

このように書いたファイルをinit.rbからrequireすることで、新たにIssueクラスに新しいメソッドを追加することができます。

重要なポイントは、Issueクラスという既存のクラスへの変更が、既存のファイルであるapp/models/issue.rbをまったく変更することなく可能となっている点です。

これは、Issueクラスのメソッドの全体が分かりにくいという大きな欠点もできますが、プラグインを配布する際に、特定のディレクトリにファイルを展開するだけでよいという、モジュールの可搬性という点において大きな利点を生むことになります。

実際のファイルは、以下のようにして、インスタンスメソッドやクラスメソッドを分離して記述することが多いです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# vendor/plugins/redmine_sample_plugin/lib/test.rb
# init.rb で require される。
 
module SamplePluginPatch
  def self.included(base)
    # (1) インスタンスメソッドを追加
    base.send(:include, InstanceMethod)
 
    base.class_eval do
      # メソッドの上書きや削除をしたい場合は、ここで行う。
      # クラスは常にオープンなので、ここで後からメソッドを変更できる。
 
      # e.g. 下記の Issue のメソッドを上書き
      #  def to_s
      #    "#{tracker} ##{id}: #{subject}"
      #  end
      def to_s
        "<issue: #{tracker} ##{id}: #{subject} >"
      end
    end
  end
 
  module InstanceMethod
    # 追加したいメソッドの場合は、ここに記述することで、
    # (1) によりインスタンスメソッドとして追加される。
    def open?
      return !(self.closed?)
    end
  end
end
 
# 以下のコードにより、Issue クラス内で include(SamplePluginPatch) と
# 書いたかのような挙動を取らせる
Issue.send(:include, SamplePluginPatch)

app以下で上書きする

これとは別に、appディレクトリ以下に直接ファイルを置くことによって、既定の動作を上書きして変更することもできます。

例えば、app/views/issues/_edit.rhtmlの内容を少し変更したものをvendor/plugins/redmine_sample_plugin/app/views/issues/_edit.rhtmlに置くことにより、もともとの_edit.rhtmlではなく、redmine_sample_plugin以下に置かれた_edit.rhtmlを使わせることができます。

これは、Rails がファイルを検索する順序として、各プラグインのapp以下を探し、見つからなければルートディレクトリ直下のappディレクトリを探すとなっているためです。

そのため、同名のファイルをプラグインのapp以下に同じディレクトリ構成で置いてやることにより、自由にデフォルトのファイルを置き換えることができます。

簡単に表示やデフォルトの挙動をカスタマイズする程度であれば、このあたりを理解するだけで比較的自由に変更することができると思います。

一方で、あまりに自由度が高いので、これをやりすぎると、全体の見通しが悪くなったり、他のプラグインとの関係で思わぬ副作用が出たりするかもしれません。

Rails は、そもそも組込みのクラスにもいろいろなメソッドを (勝手に) 追加しています。こういう部分は、きちんとした設計のもとに行えば、非常に使い勝手が上がりますが、下手に追加すると、混乱を招くだけです。

Rails では、きちんとした設計のもとに行うことで、利便性を実現しています。

それと同じように、プラグインに対してもユーザーが自由にカスタマイズできるようにしておき、「自己責任でプログラマが自由にしてね」という余地をあえて残すような設計にしているのかもしれません。

0 件のコメント:

コメントを投稿