この記事は2015年に書かれたもので使用するメソッドや各種ソフトウェアのバージョンがとても古くなっているのでご注意ください。
イントロダクション
記事の目的
Ruby on Rails によるモンキーパッチの作法について記録します。また、Redmine のサンプルプラグインを開発しながら実践します。
Redmine とは?
Ruby on Rails で書かれたオープンソースのプロジェクト管理ソフトウェアです。
http://redmine.jp
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
さて今回は、プロジェクト一覧画面におけるプロジェクトツリーの上に、任意の重要プロジェクトを表示する拡張を行います。まあ、常に上位表示しておきたいプロジェクトを設定するためのプラグイン、というイメージです。
任意のプロジェクトは、プロジェクトの設定画面へチェックボックスを追加して、設定することにします。
Redmine 本体とプラグインスケルトンの準備
Redmine 本体を clone する
では、さっそく始めましょう。Redmine のリポジトリは下記です。
https://github.com/redmine/redmine
git clone https://github.com/redmine/redmine.git
bundle install
bundle
secret_key_base の設定
まず、config/secrets.yml
を作成します。
cd redmine
touch config/secrets.yml
下記コマンドを入力し、secret_key を出力してもらいます。
bundle exec rake secret
2d32ad430fd28c65781315a156211c031c8a72883bd966...
今出力された長いランダムな文字列を、config/secrets.yml
に設定します。
development:
secret_key_base: 2d32ad430fd28c65781315a156211c031c8a72883bd966...
データベースの設定ファイル作成
# config/dababase.yml
はご使用の環境に合わせて設定してください。
cp config/database.yml.example config/database.yml
bundle install
bundle
データベースの作成
bundle exec rake db:create
bundle exec rake db:migrate
この辺で rails s
してサーバーの起動確認をしておくと良いでしょう。
プラグインスケルトン作成
今回は show_important_projects
という名前のプラグインを作成します。
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
というカラムを設けることにします。
モデル/マイグレーションファイルの作成
bundle exec rails g redmine_plugin_model show_important_projects ImportantProject project:belongs_to important:boolean
生成されたマイグレーションクラス(編集不要)
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
コマンドとは少し異なるので注意が必要です。
bundle exec rake redmine:plugins:migrate NAME=show_important_projects
コアのモデルを拡張して関連を設定
コアの Project
モデルと、今作成した ImportantProject
モデルを has_one
/ belongs_to
の関連で設定します。
ImportantProject モデル
まずは、ImportantProject モデルに設定しましょう。いつも通りですね。
class ImportantProject < ActiveRecord::Base
unloadable
belongs_to :project
end
Project モデル
さて、Project
モデルは、コアで実装されているモデルなので、ここでようやく Ruby on Rails のパッチングについてお話することができます。とはいえ、業務経験4ヶ月程度のクソザコがあれこれ説明するよりは、読んだ方が早いコードに仕上がっていると思うので、まずはご覧頂くことにします。
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
するようにしておきます。
+ 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">
の一番下にある、こいつです。
<%= call_hook(:view_projects_form, :project => @project, :form => f) %>
これは、パッチングが楽に済む例です。このフックを利用して、View を拡張します。
フック箇所に部分テンプレートを差し込む準備
Redmine の View フックを利用して部分テンプレートを差し込むのは非常に簡単です。このように、:view_projects_form
のフックに対して、差し込む部分テンプレートを指定するだけです。
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
included do
unloadable
has_one :important_project, dependent: :destroy
+ safe_attributes :important_project_attributes
+ accepts_nested_attributes_for :important_project
end
class ImportantProject < ActiveRecord::Base
unloadable
belongs_to :project
+ attr_accessible :important
end
safe_attributes
, attr_accessible
は Strong Parameters
に対応していない Redmine のためのおまじないです。最後に、先ほどのフックに差し込む部分テンプレートを書いていきます。
<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
するためのヘルパーのようです。
# 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
メソッドを拡張するわけですが、
- コードの上書きは最小限に
の法則に従い、オーバーライドは避けることにします。そこで役に立つのが、ActiveSupport
の alias_method_chain
です。
http://apidock.com/rails/ActiveSupport/CoreExtensions/Module/alias_method_chain
さっそく使ってみましょう。Project
モデルクラスと同様に、ProjectHelper
モジュールの継承ツリーに自作のパッチを加えます。
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
を追加して
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
はこんな感じになりました。
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つ並んでいて文書構造が不自然です。プロジェクト
と書いた見出しを、下に移動させてしまうのが良い気がしますね。
しかし、この見出しは render_project_hierarchy
で作っているわけではありません。コアの app/views/projects/index.html.erb
に書かれているのです。簡潔に済ませるなら、app/views/projects/index.html.erb
を上書きして見出しを削除し、render_project_hierarchy
で適切な場所に差し込めば OK です。ただしパッチングの良い作法は
- コードの上書きは最小限に
です。ただでさえ拡張されやすそうな app/views/projects/index.html.erb
を、こんなつまらないことで上書きしたくはありません。ここで新たな View 拡張方法を紹介します。
Javascript による View の拡張
フロント側で Javascript(jQuery) によって DOM を書き換える方法を試してみます。javascript_include_tag
を該当の View に埋め込むことで、該当の画面に Javascript のコードを埋め込みます。javascript_include_tag
は、該当 View のフックにて埋め込みますが、今苦労している通り、拡張している画面にフックは存在しないため、先ほどの render_project_hierarchy
を利用します。
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
要素の真下に移動します。
(function () {
$('#content > h2:first').insertAfter($('hr#break_important_projects'));
}());
このようなコードを書く際にも、最も共存するパッチへの影響が少なく済むように意識する必要があります。例えば今回のサンプルプラグインでは、hr
要素に id
を付加して、insertAfter
に渡す要素を確実なものにしています。
ただし今回の場合、共存することになったプラグインに div#content
へ別の h2
要素を差し込まれてしまったら、意図した動きになりません。そんなリスクを常に抱えつつも、できる限りリスクを減らせるように、気を使うことが重要です。
さて、長々とした記事になってしまいましたが、これでサンプルプラグインは完成です。
まとめ
今回はパッチングに必要な3つの作法について、記録しました。
- コードの上書きは最小限に
- データベーススキーマへの影響は最小限に
- 名前空間の汚染は最小限に
私はまだ Ruby on Rails を始めて間もないため経験はありませんが、Redmine のプラグイン開発だけでなく、他のプラグイン機構を提供している Ruby on Rails アプリケーションのプラグイン開発や Gem の拡張などで、これらの作法が役に立つのではないかと思います。
以上です、ありがとうございました。