Redmineで複数のプラグインを共存させたり、開発する場合の勘所

Redmineプラグイン開発。

詳しくわかってない人が詳しくわかってないなりに、複数のプラグインを導入する場合のプラグイン開発において、ハマったポイントとかを交え、勘所としてまとめておく。

プラグイン開発のTipsやチュートリアルは公式を参照。

developmentモードで操作しない?

公開されているプラグインには、「productionモードでしか動作しない」と明記されているものも少なくない。

この辺りは色々とググると出てくるので省略するけど、結論として、自作するプラグイン(のコントローラ)にはunloadableを書いておくようにするのがよい。

ついでにいうと、プラグインで既存のコントローラを拡張する場合にも書いておいたほうがいい…のかな…(下記)

module FooControllerPatch #:nodoc:
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      unloadable

    end
  end
  (snip)
end

ただ、「productionモードでしか動作しない」ことを明記している他のプラグインが、上記原因だけで説明できるものなのかといえば、そうでもないっぽい。う〜ん…。実際ためしてみると動いてるようなものもあるし。

新規にメソッドを追加して呼び出したらundefined methodエラー

自作のプラグイン(foo)で、CustomFieldsHelperを拡張して、既存のメソッドCustomFieldsHelper#show_value(custom_value)の置き換えと、新しいメソッドの追加をした場合に、undefined methodエラーが起こるようになった。

plugins/foo/lib/foo_custom_fields_helper_patch.rb
module FooCustomFieldsHelperPatch #:nodoc:
  def self.included(base)
    base.send(:include, InstanceMethods)

    base.class_eval do
      unloadable

      alias_method_chain :show_value, :foo      
    end
  end
  
  module InstanceMethods
    def show_value_with_foo(custom_value)
      hoge(custom_value)
    end

    def hoge(custom_value)
      (snip)
    end
  end
end

この時に、新しいチケットを登録すると、undefined methodエラーが起きる。ちなみに、CustomFieldsHelper#show_valueは、カスタムフィールドの登録値を画面表示用に変換して返すメソッドである。

新しいチケット登録後はチケットの詳細画面が表示されるので、問題があれば、チケット一覧からチケット詳細画面に遷移した場合にも起きるはずだが、エラーは起きず、正しく表示された。

結局、エラーが起こっている箇所は、メール送信の時のviewのレンダリングだった。Redmineで使っているメール送信のためのモデルは以下のように書かれている。

app/models/mailer.rb:18
class Mailer < ActionMailer::Base
  layout 'mailer'
  helper :application
  helper :issues
  helper :custom_fields

ここでCustomFieldsHelperが使われるのがミソかなと。現象から逆算すると、プラグインでCustomFieldsHelperが拡張される前に、Mailerがロードされており、その時点でMailerは拡張前のCustomFieldsHelperをインクルードしている、ってことなのかなぁ。でもなんか腑に落ちない。

適切な修正方法は不明…。エラーでチケットが登録できないのは致命的なので、とりあえずはrescueでエラーを拾っておくようにして回避した。

プラグインのロード順によってバリデーションの機嫌が異なる

カスタムフィールドの書式を追加するプラグインを作っていて、既存のプラグインを真似て作っても、常に「書式は一覧にありません。」エラーが出てしまい、カスタムフィールドの登録ができなくなってしまった問題も起きた。

このエラーメッセージは、カスタムフィールドクラスで定義されている以下のバリデーションに違反してしまっているため。

app/models/custom_field.rb:28
  validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats

この問題で厄介だったのは、カスタムフィールドの書式を追加するプラグインを複数作った場合、きちんと登録できるものと登録できないものが存在したこと。print debugすると、Redmine::CustomFieldFormat.available_formatsには新しく作った書式が含まれていた。

他のプラグインとの競合が原因だった。他のプラグインをアンインストールしたり、自作のプラグイン以外のプラグイン名に"z_"みたいなサフィックスをつけて、自作のプラグインよりも後にロードされるようにしたところ、すべてのカスタムフィールドの書式で問題なく登録できたので。

あるいは自作プラグインのinit.rbで無理やりCustomFieldクラスをアンロードして、再ロードする、みたいなことをやっても問題なく登録できるようになった。

つまり、他のプラグインがCustomFieldクラスやCustomFieldFormatクラスをロードしたタイミングで、書式(Redmine::CustomFieldFormat.available_formats)が確定してしまっているみたい。

alias_method_chainで無限ループエラー

これは原因が簡単で複数のプラグインで同じメソッドをalias_method_chainで置き換えていた場合に、機能名が衝突していたというもの。

プラグインfooのFooPatchモジュールとプラグインbarのBarPatchモジュールで、同じクラスに対して

alias_method_chain :do_something, :xxx

みたいに書いてしまうと、簡単にこの問題は起こるので注意。

拡張モジュールの名前が衝突すると一つしか有効にならない

既存のクラス(Foo)を拡張する場合、大体以下の様な記述をプラグインのinit.rbに書く。

Rails.configuration.to_prepare do
  unless Foo.included_modules.include? FooPatch
    Foo.send(:include, FooPatch)
  end
end

ここで複数のプラグインでFooPatchという同じモジュール名を使っていた場合はどうなるか。先にロードされたプラグインのパッチのみが有効になる(予想通り?)。

なので、パッチの名前は衝突しないように工夫が必要。実はこれはViewの名前にも同じことが言える。Viewの場合は、最後にロードされたプラグインのViewが優先される。

まとめ

  • 迷ったらデバッグメッセージを埋め込んで確認。
  • なんか原因が分からなかったら、他のプラグインをアンインストール(pluginsディレクトリから移動して再起動すればOK)してみて、同様の問題が起こるかどうかも確かめてみる。
  • alias_method_chainの機能名はユニークにする。
  • viewのファイル名はユニークにする(既存のviewを上書きする場合は除く)
  • 各種拡張用モジュールの名前はユニークにする。

ここでいう「ユニーク」は、これから他のプラグインを入れたりした場合にも、絶対に被らないようなもの。てっとり早いのは、やはり、プラグイン名(やその省略形)を含めることだと思う。実際、公開されているプラグインはそうなっているものがほとんど。

Javaのクラスローダ周りもそうだけど、問題が表面的なところでなくて、少し深いところになると、原因調査とか解決が難しいなぁ…。