Redmine プラグイン開発に学ぶパッチング作法

**この記事は2015年に書かれたもので使用するメソッドや各種ソフトウェアのバージョンがとても古くなっているのでご注意ください。**

# イントロダクション
## 記事の目的
Ruby on Rails によるモンキーパッチの作法について記録します。また、[Redmine](http://redmine.jp) のサンプルプラグインを開発しながら実践します。

### Redmine とは？
Ruby on Rails で書かれたオープンソースのプロジェクト管理ソフトウェアです。
[http://redmine.jp](http://redmine.jp)
[https://github.com/redmine/redmine](https://github.com/redmine/redmine)

### モンキーパッチとは？
オリジナルのソースコード(今回は Redmine のコア)を変更することなく、ソフトウェアの振る舞いを変更する方法のことです。

## 環境
- Redmine 3.1.2
- Ruby on Rails 4.2.5
- Ruby 2.2.3


## 他のパッチと平和に共存するための3つの作法
Redmine のプラグイン開発に限らず、パッチングの作法として意識すべきことが3つあります。

### 1. コードの上書きは最小限に
Ruby の素晴らしさは、Redmine 本体のソースコード(以下"コア"と呼びます) に存在するクラスをオープンし、メソッドをオーバーライドして好き勝手に振る舞いを変更できることです。また Redmine は、プラグインに置かれている View ファイルを優先的に扱うので、ただ erb ファイルを配置しておくだけで、View の内容を書き換えることができます。

ただし、このような上書きを行ってしまうと、別のプラグインも同じファイルやメソッドを上書きしていた場合に困ります。後からロードされたもの勝ちになってしまうためです。コアのコードをガリガリ書き変えていけるのは Ruby の魅力ですが、`alias_method_chain` などといった、パッチングに適した機構が用意されているのも Ruby の魅力です。また、View に関しても上書きするより、Javascript で DOM を動的に書き換えていった方が、手間はかかりますが上書きは回避することができます。後ほどサンプルプラグインを開発しながら、具体例を紹介します。
※ 現在 `alias_method_chain` は非推奨となっています。

### 2. データベーススキーマへの影響は最小限に
プラグイン側でマイグレーションファイルを書いて、データベーススキーマを変更することができますが、できることなら既存のテーブルにカラムを追加することは避けた方が良いでしょう。例えば `projects` テーブルに何かしらのカラムを追加したくなったとしても、`projects` テーブルを拡張するのではなく、別のテーブルを作成して `belongs_to :project` した方が、より安全です。こちらも後ほど具体例を紹介します。

### 3. 名前空間の汚染は最小限に
例えば上記のように、データベースに新たなテーブルを追加する場合や、新たなモデルクラスといったグローバルスコープな識別子は、別のプラグインと衝突しないように注意が必要です。最も安易な衝突回避策は、識別子にプラグイン名のプレフィックスを設けることです。ただ、あまり美しくないのでケースバイケースということにしまして、今回のサンプルプラグインの開発では、そこまでの考慮はしませんが、衝突の危険性は孕んでいることだけ意識しておく必要があります。


# サンプルプラグイン開発
## 完成イメージ
上記3点のパッチング作法の実践として、早速 Redmine のサンプルプラグインを開発してみます。とてもシンプルなプラグインですが、手順はそこそこ多いので、パッチング作法の本質的でない説明が多くなります。とはいえ、プラグイン開発について真面目に学ぶには説明足らずな中途半端な記事であることをご容赦ください。

これから開発するサンプルプラグインのソースコードは、下記で公開しています。
[https://github.com/ogihara-ryo/show_important_projects](https://github.com/ogihara-ryo/show_important_projects)

さて今回は、プロジェクト一覧画面におけるプロジェクトツリーの上に、任意の重要プロジェクトを表示する拡張を行います。まあ、常に上位表示しておきたいプロジェクトを設定するためのプラグイン、というイメージです。
![完成図.png](https://qiita-image-store.s3.amazonaws.com/0/102542/96f78243-2912-a2c9-10ff-2f0e92685759.png)


任意のプロジェクトは、プロジェクトの設定画面へチェックボックスを追加して、設定することにします。
![重要プロジェクト設定.png](https://qiita-image-store.s3.amazonaws.com/0/102542/136bd6b7-f739-59c1-8b31-6af97d19d647.png)


## Redmine 本体とプラグインスケルトンの準備
### Redmine 本体を clone する
では、さっそく始めましょう。Redmine のリポジトリは下記です。
[https://github.com/redmine/redmine](https://github.com/redmine/redmine)

```bash:shell
git clone https://github.com/redmine/redmine.git
```


### bundle install

```bash:shell
bundle
```


### secret_key_base の設定
まず、`config/secrets.yml` を作成します。

```bash:shell
cd redmine
touch config/secrets.yml
```

下記コマンドを入力し、secret_key を出力してもらいます。

```bash:shell
bundle exec rake secret
2d32ad430fd28c65781315a156211c031c8a72883bd966...
```

今出力された長いランダムな文字列を、`config/secrets.yml` に設定します。

```yaml:config/secrets.yml
development:
  secret_key_base: 2d32ad430fd28c65781315a156211c031c8a72883bd966...
```


### データベースの設定ファイル作成
\# `config/dababase.yml` はご使用の環境に合わせて設定してください。

```bash:shell
cp config/database.yml.example config/database.yml
```


### bundle install

```bash:shell
bundle
```


### データベースの作成
```bash:shell
bundle exec rake db:create
bundle exec rake db:migrate
```

この辺で `rails s` してサーバーの起動確認をしておくと良いでしょう。


### プラグインスケルトン作成
今回は `show_important_projects ` という名前のプラグインを作成します。

```bash:shell
bundle exec rails g redmine_plugin show_important_projects
cd show_important_projects
```


## 重要プロジェクトモデルの追加
さて、重要なプロジェクトを上位表示するプラグインを作成しますので、重要かどうかを判定する論理型のカラムを作成する必要があります。単純に考えれば、`projects` テーブルに `add_column` するマイグレーションを書いて。。と考えるところですが、

> 2.データベーススキーマへの影響は最小限に

の法則に従うことにしましょう。今回は `important_projects` テーブルを新たに作成し、`projects` テーブルと `has_one` / `belongs_to` の関連を設定し、そのテーブルに `important` というカラムを設けることにします。

### モデル/マイグレーションファイルの作成
```bash:shell
bundle exec rails g redmine_plugin_model show_important_projects ImportantProject project:belongs_to important:boolean
```

### 生成されたマイグレーションクラス(編集不要)
```rb:db/migrate/001_create_important_projects.rb
class CreateImportantProjects < ActiveRecord::Migration
  def change
    create_table :important_projects do |t|
      t.belongs_to :project, index: true, foreign_key: true
      t.boolean :important, default: false, null: false
    end
    add_index :important_projects, :project_id
  end
end
```

### migrate コマンド実行
では、このまま `migrate` コマンドを実行します。通常の Ruby on Rails アプリケーションの `migrate` コマンドとは少し異なるので注意が必要です。

```bash:shell
bundle exec rake redmine:plugins:migrate NAME=show_important_projects
```



## コアのモデルを拡張して関連を設定
コアの `Project` モデルと、今作成した `ImportantProject` モデルを `has_one` / `belongs_to` の関連で設定します。

### ImportantProject モデル
まずは、ImportantProject モデルに設定しましょう。いつも通りですね。

```rb:app/models/important_project.rb
class ImportantProject < ActiveRecord::Base
  unloadable
  belongs_to :project
end
```

### Project モデル
さて、`Project` モデルは、コアで実装されているモデルなので、ここでようやく Ruby on Rails のパッチングについてお話することができます。とはいえ、業務経験4ヶ月程度のクソザコがあれこれ説明するよりは、読んだ方が早いコードに仕上がっていると思うので、まずはご覧頂くことにします。

```rb:lib/show_important_projects/project_patch.rb
require_dependency 'project'

module ShowImportantProjects
  module ProjectPatch
    extend ActiveSupport::Concern

    included do
      unloadable
      has_one :important_project, dependent: :destroy
    end

    def important?
      important_project.try(:important?)
    end
  end
end

ActionDispatch::Reloader.to_prepare do
  unless Project.included_modules.include?(ShowImportantProjects::ProjectPatch)
    Project.send(:include, ShowImportantProjects::ProjectPatch)
  end
end
```

末尾の5行で、コアの `Project` クラスに対して、`ShowImportantProjects::ProjectPatch` を `include` します。このとき

> 3.名前空間の汚染は最小限に

の法則に従って、パッチモジュールを `ShowImportantProjects` でラップしておきます。


`ShowImportantProjects::ProjectPatch` では、先ほど作成した `important_projects` テーブルとの関連を設定しています。この関連を利用して、`important?` と聞けば、自分が重要かどうか返してくれるように拡張してみました。

このファイルは `init.rb` から `require_dependency` します。これからもパッチファイルは増えていくので、`lib/show_important_projects` ディレクトリ配下の全 Ruby ファイルを `require_dependency` するようにしておきます。

```diff:init.rb
+ Dir[File.expand_path('../lib/show_important_projects', __FILE__) << '/*.rb'].each do |file|
+   require_dependency file
+ end


  Redmine::Plugin.register :show_important_projects do
    name 'Show Important Projects plugin'
    author 'Author name'
    description 'This is a plugin for Redmine'
    version '0.0.1'
    url 'http://example.com/path/to/plugin'
    author_url 'http://example.com/about'
  end
```

これで、コアの `Project` モデルクラスにパッチをあてることができました。`rails c` で `Project` モデルのインスタンスから `important?` できるか確認してみると良いでしょう。ちなみに `Project` モデルのインスタンスをコンソールから作るのは結構しんどいので、ブラウザーから一旦作成しておいた方が良いです。


## フックが用意されている View の拡張
さて、これからプロジェクト設定画面(新規作成画面含)に重要プロジェクトか否かを設定するチェックボックスを設置します。拡張対象の View ファイルは、コア側の `app/views/projects/_form.html.erb` です。

各種プロジェクト設定項目の下に、チェックボックスを1つ追加するのは、とても簡単です。各種プロジェクト設定項目の下に、フックが用意されているためです。`<div class="box tabular">`の一番下にある、こいつです。

```erb:app/views/projects/_form.html.erb
<%= call_hook(:view_projects_form, :project => @project, :form => f) %>
```

これは、パッチングが楽に済む例です。このフックを利用して、View を拡張します。

### フック箇所に部分テンプレートを差し込む準備
Redmine の View フックを利用して部分テンプレートを差し込むのは非常に簡単です。このように、`:view_projects_form` のフックに対して、差し込む部分テンプレートを指定するだけです。

```rb:lib/show_important_projects/hooks.rb
module ShowImportantProjects
  class Hooks < Redmine::Hook::ViewListener
    render_on :view_projects_form, partial: 'projects/hook_view_projects_form'
  end
end
```


### 重要プロジェクト設定保存機能の実装
コアに定義されているテーブルの関連テーブルを操作したいときは、`:accepts_nested_attributes_for` を利用すると幸せになれます。
[http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html](http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html)

```diff:lib/show_important_projects/project_patch.rb
    included do
      unloadable
      has_one :important_project, dependent: :destroy
+     safe_attributes :important_project_attributes
+     accepts_nested_attributes_for :important_project
    end
```

```diff:app/models/important_project.rb
  class ImportantProject < ActiveRecord::Base
    unloadable
    belongs_to :project
+   attr_accessible :important
  end
```

`safe_attributes`, `attr_accessible` は `Strong Parameters` に対応していない Redmine のためのおまじないです。最後に、先ほどのフックに差し込む部分テンプレートを書いていきます。

```erb:app/views/projects/_hook_view_projects_form.html.erb
<p>
  <label for='important'>重要プロジェクト</label>
  <%# check_box_tag は未チェック時にパラメーターを送らないため、hidden_field_tag を使用 %>
  <%= hidden_field_tag 'project[important_project_attributes][important]', false %>
  <%= check_box_tag 'project[important_project_attributes][important]', true, @project.important?, id: 'important' %>
</p>
```

## フックが用意されていない View の拡張
さて、ではプロジェクト一覧画面(`projects/index`)の上部に、重要プロジェクトを表示するための拡張を行います。ここで、1つ問題があります。なんと、コアの `app/views/projects/index.html.erb` 、及びそこから `render` される部分テンプレートには、フックが用意されていません。

全画面共通の `:view_layouts_base_content` というフックは用意されているので、`context[:controller].controller_name == 'projects' && context[:controller].action_name == 'index'` の判定を行ってプロジェクト一覧画面のみを拡張させることも可能ですが、全画面共通のフックに、`projects/index` 専用の処理を書くのは気持ちが悪いものです。

### 差し込めそうな場所を探す
途方に暮れながら `app/views/projects/index.html.erb` を読んでいると、`render_project_hierarchy` という、何やらいい感じのヘルパーを見つけました。プロジェクト一覧を階層的に `render` するためのヘルパーのようです。

```rb:app/helpers/projects_helper.rb
  # Renders the projects index
  def render_project_hierarchy(projects)
    render_project_nested_lists(projects) do |project|
      s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
      if project.description.present?
        s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
      end
      s
    end
  end
```

コアの膨大なコードから、パッチングに適した箇所を探し出すのは茨の道です。「ここに書くしかないのかな。。」といった妥協の元、選ぶ羽目になることも少なくありません。まあ、今回はサンプルプラグインなので気にせず話を進めます。


## alias_method_chain によるメソッドの拡張
さて、この `app/helpers/projects_helper.rb` の `render_project_hierarchy` メソッドを拡張するわけですが、

> 1. コードの上書きは最小限に

の法則に従い、オーバーライドは避けることにします。そこで役に立つのが、`ActiveSupport` の `alias_method_chain` です。
[http://apidock.com/rails/ActiveSupport/CoreExtensions/Module/alias_method_chain](http://apidock.com/rails/ActiveSupport/CoreExtensions/Module/alias_method_chain)

さっそく使ってみましょう。`Project` モデルクラスと同様に、`ProjectHelper` モジュールの継承ツリーに自作のパッチを加えます。

```rb:lib/show_important_projects/projects_helper_patch.rb
require_dependency 'projects_helper'

module ShowImportantProjects
  module ProjectsHelperPatch
    extend ActiveSupport::Concern

    included do
      alias_method_chain :render_project_hierarchy, :show_important_projects
    end

    def render_project_hierarchy_with_show_important_projects(projects)
      render_project_hierarchy_without_show_important_projects
    end
  end
end

ActionDispatch::Reloader.to_prepare do
  unless ProjectsHelper.included_modules.include?(ShowImportantProjects::ProjectsHelperPatch)
    ProjectsHelper.send(:include, ShowImportantProjects::ProjectsHelperPatch)
  end
end
```

`alias_method_chain` は、`第一引数_with_第二引数` といった名前でメソッドのエイリアスを作成しつつ、オリジナルのメソッドを `第一引数_without_第二引数` に退避します。`render_project_hierarchy_with_show_important_projects` と少し長い名前になってしまいましたが、これらのメソッド名が衝突したら本末転倒なので、目を瞑っておきます。

`render_project_hierarchy` は DOM を返すメソッドなので、オリジナルの `render_project_hierarchy_without_show_important_projects` を呼ぶ前に、重要プロジェクト一覧の DOM を作って詰めてしまえば OK です。

`Project` モデルに重要プロジェクトのみを返す `scope` を追加して

```diff:lib/show_important_projects/project_patch.rb
    included do
      unloadable
      has_one :important_project, dependent: :destroy
      safe_attributes :important_project_attributes
      accepts_nested_attributes_for :important_project

+     scope :importants, -> { joins(:important_project).where(important_projects: { important: true }) }
    end
```

コア側の `app/helpers/projects_helper.rb` の `render_project_hierarchy` メソッドや、`app/helpers/application_helpers.rb` の `render_project_nested_lists` メソッドを参考にしつつ、よいしょよいしょと詰めていきます。最終的に `ProjectsHelperPatch` はこんな感じになりました。

```rb:lib/show_important_projects/projects_helper_patch.rb
require_dependency 'projects_helper'

module ShowImportantProjects
  module ProjectsHelperPatch
    extend ActiveSupport::Concern

    included do
      alias_method_chain :render_project_hierarchy, :show_important_projects
    end

    def render_project_hierarchy_with_show_important_projects(projects)
      s = render_important_projects
      s << render_project_hierarchy_without_show_important_projects(projects)
      s.html_safe
    end

    private

    def render_important_projects
      s = "<h2>重要プロジェクト</h3><ul id='important_projects' class='projects root'>"
      Project.importants.each do |project|
        s << "<li class='root'><div class='root'>"
        s << link_to_project(project, {}, class: "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
        s << '</div></li>'
      end
      s << '</ul>'
      s << "<hr id='break_important_projects' style='margin: 24px 0;' />"
    end
  end
end

ActionDispatch::Reloader.to_prepare do
  unless ProjectsHelper.included_modules.include?(ShowImportantProjects::ProjectsHelperPatch)
    ProjectsHelper.send(:include, ShowImportantProjects::ProjectsHelperPatch)
  end
end
```

これで表示してみると、なかなかいい感じなのですが、見出しが2つ並んでいて文書構造が不自然です。`プロジェクト` と書いた見出しを、下に移動させてしまうのが良い気がしますね。
![見出しを移動する.png](https://qiita-image-store.s3.amazonaws.com/0/102542/3c8ba8cd-30b9-0c18-932e-00900c6957b2.png)


しかし、この見出しは `render_project_hierarchy` で作っているわけではありません。コアの `app/views/projects/index.html.erb` に書かれているのです。簡潔に済ませるなら、`app/views/projects/index.html.erb` を上書きして見出しを削除し、`render_project_hierarchy` で適切な場所に差し込めば OK です。ただしパッチングの良い作法は

> 1. コードの上書きは最小限に

です。ただでさえ拡張されやすそうな `app/views/projects/index.html.erb` を、こんなつまらないことで上書きしたくはありません。ここで新たな View 拡張方法を紹介します。


## Javascript による View の拡張
フロント側で Javascript(jQuery) によって DOM を書き換える方法を試してみます。`javascript_include_tag` を該当の View に埋め込むことで、該当の画面に Javascript のコードを埋め込みます。`javascript_include_tag` は、該当 View のフックにて埋め込みますが、今苦労している通り、拡張している画面にフックは存在しないため、先ほどの `render_project_hierarchy` を利用します。

```diff:lib/show_important_projects/projects_helper_patch.rb
    def render_project_hierarchy_with_show_important_projects(projects)
      s = render_important_projects
      s << render_project_hierarchy_without_show_important_projects(projects)
+     s << javascript_include_tag('move_head2_tag', plugin: 'show_important_projects')
      s.html_safe
    end
end
```

これで Javascript でできることなら、どんな拡張でも思いのままになりました。今回は `div#content` 内で一番始めに登場する `h2` 要素を、先ほど追加した重要プロジェクトと全プロジェクトを区切る `hr` 要素の真下に移動します。

```js:assets/javascripts/move_head2_tag.js
(function () {
  $('#content > h2:first').insertAfter($('hr#break_important_projects'));
}());
```

このようなコードを書く際にも、最も共存するパッチへの影響が少なく済むように意識する必要があります。例えば今回のサンプルプラグインでは、`hr` 要素に `id` を付加して、`insertAfter` に渡す要素を確実なものにしています。

ただし今回の場合、共存することになったプラグインに `div#content` へ別の `h2` 要素を差し込まれてしまったら、意図した動きになりません。そんなリスクを常に抱えつつも、できる限りリスクを減らせるように、気を使うことが重要です。

さて、長々とした記事になってしまいましたが、これでサンプルプラグインは完成です。
![完成図.png](https://qiita-image-store.s3.amazonaws.com/0/102542/96f78243-2912-a2c9-10ff-2f0e92685759.png)


# まとめ
今回はパッチングに必要な3つの作法について、記録しました。
1. コードの上書きは最小限に
2. データベーススキーマへの影響は最小限に
3. 名前空間の汚染は最小限に

私はまだ Ruby on Rails を始めて間もないため経験はありませんが、Redmine のプラグイン開発だけでなく、他のプラグイン機構を提供している Ruby on Rails アプリケーションのプラグイン開発や Gem の拡張などで、これらの作法が役に立つのではないかと思います。

以上です、ありがとうございました。