Redmine PluginのView Hookがどこにあるか列挙、完全版

Redmine plugin には、view hook と呼ばれる View の拡張ポイントがある。
それを視覚的に見る試みとして [@digdagdag さんによる研究][digdagdag] があるが、hook 自体は手作業で入れている。これを漏らすことなく自動的にやりたい。

[digdagdag]: https://qiita.com/digdagdag/items/424ae1055d66d4672ca1 "Redmine PluginのViewの拡張ポイント（View hook）がどこにあるか？を列挙する"

# TL;DR

    % cd plugins
    % git clone https://github.com/cat-in-136/redmine_view_hook_for_dev
    % cd ..
    % bundle exec rails s

![projects_test_issues.png](https://qiita-image-store.s3.amazonaws.com/0/62065/048bc877-0756-6a77-7f84-b14fbcb02ff0.png)

# Hook のリスト（却下）

Redmine における hook 呼び出しのコアの処理は下記のような非常にシンプルな仕組みとなっている:

```ruby:/lib/redmine/hook.rb
      def hook_listeners(hook)
        @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
      end

# snip

      def call_hook(hook, context={})
        [].tap do |response|
          hls = hook_listeners(hook)
          if hls.any?
            hls.each {|listener| response << listener.send(hook, context)}
          end
        end
      end
```

すなわち、登録されたリスナの中から `hook` というメソッドが定義されたリスナに対して順次そのメソッドを呼んで結果を配列に格納していっているだけである。

コンソールなどから `Redmine::Hook.listeners` でリスナが取れるのでそこからメソッドを取ることは可能ではあると思われる。メソッドの一覧から、例えば `to_s` といった hook 用途以外のものを取り除いたりする作業が必要であり、これはなかなか面倒である。

そのため、この方法は諦めた。

なお単純にリストアップするだけならば [redmine の公式Wikiの Hooks List](https://www.redmine.org/projects/redmine/wiki/Hooks_List)としては、下記の通り grep をしてくれと言っている。普段使いはこれで良いが、機械的に扱うには、これからさらにテキスト処理が必要なのでこれも却下とした。

> On Redmine 2.0.0 and up the rake task has been deprecated.
> As an alternative you can use either of the following in the Redmine directory (see RE: hook list?).
> 
>     grep -r call_hook *                                       # list of source lines with hook calls
>     grep -rohT 'call_hook([^)]*)'                             # list of hooks calls and source files
>     grep -roh  'call_hook([^)]*)' | sort -u | grep '([^)]*)'  # list of hooks calls only

# 解決策：パッチ

結局 `call_hook` をパッチする解決策を取ることにした。

最終結果は非常にシンプルで、`call_hook` に対して下記のメソッドを prepend することとし、`call_hook` の戻り値の最初の要素に常に hook 名を表示する HTML コードを含まれるようにした。

```ruby
    def call_hook(hook, context={})
      [
        "<p class=\"redmine_view_hook_for_dev\">#{hook}</p>",
        *(super(hook, context)),
      ]
    end
```

なお prepend する際は、若干注意が必要で、ここの `call_hook` はクラスメソッドのため `Redmine::Hook` に対して `prepend()` するのではなくて、`Redmine::Hook.singleton_class` に対して `prepend()` する必要がある。

# 外見

[@digdagdag さんによる研究][digdagdag]において、

> そして黒文字にしてしまったので、見づらい。

とあったので、赤太字とした。

また、開発者ツール(F12)で `$(".redmine_view_hook_for_dev")` で探し出せる利便性を確保するため CSS クラスを割り当て、CSS でスタイリングすることにした。CSS スタイルの適用そのものは、一般的な方法で `view_layouts_base_html_head` に hook メソッドを入れることで実現した。

# プラグイン化（手抜き）

以上のように述べた内容を簡素なプラグイン化してまとめた。https://github.com/cat-in-136/redmine_view_hook_for_dev

データベースも設定も何もないので、開発に必要なときに plugins フォルダにおいて、不要になったら消すという使い方を想定している。

なお、面倒だったので手抜きな作りにしていて、すべてのロジックを `init.rb` に入れ込んでいるし、`ActionDispatch::Reloader` とかも使っていない。

# 参考文献

 * @digdagdag, "[Redmine PluginのViewの拡張ポイント（View hook）がどこにあるか？を列挙する][digdagdag]", Qiita, 2014-10-07
 * "[Hooks List](https://www.redmine.org/projects/redmine/wiki/Hooks_List)", Redmine Wiki
 * [/lib/redmine/hook.rb](https://github.com/redmine/redmine/blob/3.4.3/lib/redmine/hook.rb), Redmine source code
 * [ruby - How to prepend classmethods](https://stackoverflow.com/questions/18683750/how-to-prepend-classmethods), Stackoverflow, 2017-05-23