Redmine Plugin で ApplicationHelper を拡張する際の注意
Windows PC の調子が悪いので、NPAPI プラグインの作り方は少し延期です。
今回は、Redmine 1.3.0もリリースされたので、 Redmine Plugin で ApplicationHelper を拡張する際に悩んだ問題について書きます。
Redmine プラグインで、既存のクラスやモジュールを拡張する
Redmine プラグインを作成し、 ApplicationHelper などの既存のクラスやモジュールを拡張する場合、基本的には以下のようにします。
- プラグインの
lib
以下にredmine_sample_plugin_application_helper_patch.rb
のようなファイルを作成し、以下のように記述します。1234567891011121314151617181920212223require_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)
init.rb
内部で、redmine_sample_plugin_application_helper_patch.rb
をrequire
します。
上に書いたように、一般的にはApplicationHelper
を再定義せずに、追加したり置換したりしたいメソッドを別モジュールにまとめた上で、これをインクルードさせることが多いです。
View のフック関数からの呼び出し
Redmine のプラグインへのフレームワークとして、View の決められた場所に、プラグインのコードを埋め込む、という仕組みがあります。
ここでは説明しませんが、知りたい方はRedmine自体に手を入れずに見た目を変更する方法などをご覧ください。
いずれにせよ、Redmine::Hook::ViewListener
を継承したクラスを作成し、その中にあらかじめ定められた名前のフックメソッドを定義することで、View に任意のコードを追加できます。
View のフック関数から、追加したメソッドの呼び出し
上のSamplePluginApplicationHelperPatch
のように定義した場合、新規に定義したメソッド (上の例ではsample_instance_method
) は、View のフック関数から呼ぶことができません。
ApplicationHelper 内のメソッドは、すべてのコントローラーや View から呼べるはずなのですが、呼ぶことができないのです。
呼び出せない理由
ApplicationHelper
は、Redmine::Hook::ViewListener
内で適切にinclude
されています。
また、上で定義したサンプルでは、確かにSamplePluginApplicationHelperPatch::InstanceMethod
がApplicationHelper
にインクルードされています。
そのため、間接的にSamplePluginApplicationHelperPatch::InstanceMethod
はRedmine::Hook::ViewListener
からインクルードされているように思えます。
しかし、実際にはそうなりません。
Redmine::Hook::ViewListener
がApplicationHelper
をインクルードするのは、プラグインの各ファイルがロードされるよりも早いタイミングで行われます。
そのため、Redmine::Hook::ViewListener
がApplicationHelper
をインクルードする頃には、まだSamplePluginApplicationHelperPatch::InstanceMethod
はApplicationHelper
にインクルードされていません。
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.ancestors
にC
が含まれてもよさそうですが、含まれません。
これは奇妙なように思いますが、 Ruby の仕様であり、どうしようもありません。
余談ですが、 Ruby 作者のまつもとさんも、これは奇妙に思っているらしく、Matzにっきの Traits の項目に、今後mix
を追加しようと思っている旨が書かれています。
そのため、もともとの Redmine の ViewListener に関しても同様に、Redmine::Hook::ViewListener
がApplicationHelper
をインクルードするタイミングでは、まだSamplePluginApplicationHelperPatch::InstanceMethod
がApplicationHelper
にインクルードされていないので、結果として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 件のコメント:
コメントを投稿