SyntaxHighlighter

2011年12月12日月曜日

Redmine Plugin で ApplicationHelper を拡張する際の注意

Redmine Plugin で ApplicationHelper を拡張する際の注意

Windows PC の調子が悪いので、NPAPI プラグインの作り方は少し延期です。

今回は、Redmine 1.3.0もリリースされたので、 Redmine Plugin で ApplicationHelper を拡張する際に悩んだ問題について書きます。

Redmine プラグインで、既存のクラスやモジュールを拡張する

Redmine プラグインを作成し、 ApplicationHelper などの既存のクラスやモジュールを拡張する場合、基本的には以下のようにします。

  1. プラグインのlib以下にredmine_sample_plugin_application_helper_patch.rbのようなファイルを作成し、以下のように記述します。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    require_dependency("application_helper")
     
    module SamplePluginApplicationHelperPatch
      def self.included(base)
        base.send(:include, InstanceMethod)
     
        base.class_eval do
          # 既存のメソッドを置き換えたりする場合は、
          # ここで alias_method などを使う。
        end
      end
     
      module InstanceMethod
        # ApplicationHelper に新規追加するメソッドを記述する。
        def sample_instance_method
          # 何らかの処理
        end
      end
    end
     
    # ApplicationHelper モジュールに SamplePluginApplicationHelperPatch を
    # インクルードさせる。
    ApplicationHelper.send(:include, SamplePluginApplicationHelperPatch)
  2. init.rb内部で、redmine_sample_plugin_application_helper_patch.rbrequireします。

上に書いたように、一般的にはApplicationHelperを再定義せずに、追加したり置換したりしたいメソッドを別モジュールにまとめた上で、これをインクルードさせることが多いです。

View のフック関数からの呼び出し

Redmine のプラグインへのフレームワークとして、View の決められた場所に、プラグインのコードを埋め込む、という仕組みがあります。

ここでは説明しませんが、知りたい方はRedmine自体に手を入れずに見た目を変更する方法などをご覧ください。

いずれにせよ、Redmine::Hook::ViewListenerを継承したクラスを作成し、その中にあらかじめ定められた名前のフックメソッドを定義することで、View に任意のコードを追加できます。

View のフック関数から、追加したメソッドの呼び出し

上のSamplePluginApplicationHelperPatchのように定義した場合、新規に定義したメソッド (上の例ではsample_instance_method) は、View のフック関数から呼ぶことができません

ApplicationHelper 内のメソッドは、すべてのコントローラーや View から呼べるはずなのですが、呼ぶことができないのです。

呼び出せない理由

ApplicationHelperは、Redmine::Hook::ViewListener内で適切にincludeされています。
また、上で定義したサンプルでは、確かにSamplePluginApplicationHelperPatch::InstanceMethodApplicationHelperにインクルードされています。
そのため、間接的にSamplePluginApplicationHelperPatch::InstanceMethodRedmine::Hook::ViewListenerからインクルードされているように思えます。

しかし、実際にはそうなりません。

Redmine::Hook::ViewListenerApplicationHelperをインクルードするのは、プラグインの各ファイルがロードされるよりも早いタイミングで行われます。
そのため、Redmine::Hook::ViewListenerApplicationHelperをインクルードする頃には、まだSamplePluginApplicationHelperPatch::InstanceMethodApplicationHelperにインクルードされていません。

Ruby では、includeしたタイミングで継承関係が決定します。

例を挙げると、以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module A
end
 
module B
  include(A)
end
 
module C
end
 
p B.ancestors
# => [ B, A ]
 
A.send(:include, C# A.ancestors に C を追加
p B.ancestors
# => [ B, A ]     # C は含まれません。
 
module D
  include(A)
end
p D.ancestors
# => [ D, A, C ]  # C が含まれます。

上の例で、B.ancestorsCが含まれてもよさそうですが、含まれません。

これは奇妙なように思いますが、 Ruby の仕様であり、どうしようもありません。

余談ですが、 Ruby 作者のまつもとさんも、これは奇妙に思っているらしく、Matzにっきの Traits の項目に、今後mixを追加しようと思っている旨が書かれています。

そのため、もともとの Redmine の ViewListener に関しても同様に、Redmine::Hook::ViewListenerApplicationHelperをインクルードするタイミングでは、まだSamplePluginApplicationHelperPatch::InstanceMethodApplicationHelperにインクルードされていないので、結果としてRedmine::Hook::ViewListenerの ancestors にはSamplePluginApplicationHelperPatch::InstanceMethodが含まれないことになります。

回避策

これは、例えば以下のようにすれば回避することができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require_dependency("application_helper")
 
module SamplePluginApplicationHelperPatch
  def self.included(base)
    base.send(:include, InstanceMethod)
 
    base.class_eval do
      ###########################
      # alias_method で ApplicationHelper にメソッドを追加
      alias_method :sample_instance_method, :def_sample_instance_method
    end
  end
 
  module InstanceMethod
    # ApplicationHelper に新規追加するメソッドを記述する。
    def def_sample_instance_method
      # 何らかの処理
    end
  end
end
 
# ApplicationHelper モジュールに SamplePluginApplicationHelperPatch を
# インクルードさせる。
ApplicationHelper.send(:include, SamplePluginApplicationHelperPatch)

このようにすることで、sample_instance_method自体はApplicationHelperに直接追加されるので、Redmine::Hook::ViewListenerからでも呼び出すことが可能になります。

もちろん、base.class_eval doの内部で、直接sample_instance_methodを定義しても呼び出すことができますが、このあたりはコードの見易さの問題で、好みの分かれるところだろうと思います。

0 件のコメント:

コメントを投稿