Rails エンジン入門

本ガイドでは、Railsのエンジンについて解説します。また、簡潔で使いやすいインターフェイスを使った、ホストアプリケーション向け追加機能についても解説します。

このガイドの内容:

1 Railsにおけるエンジンの役割

エンジン (engine) とは、アプリケーションのミニチュアのようなものであり、ホストアプリケーションに機能を提供します。Railsアプリケーションは実際にはエンジンに「ターボをかけた」ようなものにすぎず、Rails::ApplicationクラスはRails::Engineから多くの振る舞いを継承しています。

従って、エンジンとアプリケーションは、細かな違いを除けばほぼ同じものであると考えていただいてよいでしょう。本ガイドでもこの点をたびたび確認します。エンジンとアプリケーションは、同じ構造を共有しています。

エンジンは、プラグインとも密接に関連します。エンジンもプラグインも、共通のlibディレクトリ構造を共有し、どちらもrails plugin newジェネレータを使用して生成されます。両者に違いがあるとすれば、Railsはエンジンを一種の「完全なプラグイン」とみなしている点です。これは、エンジンを生成するにはジェネレータコマンドで--fullを与えることからもわかります。実際にはこのガイドでは--mountableオプションを使用します。これは--fullのオプション以外にもいくつかの機能を追加してくれます。以後本ガイドでは「完全なプラグイン (full plugin)」を単に「エンジン」と呼びます。エンジンはプラグインになることもでき、プラグインがエンジンになることもできます。

本ガイドで説明のために作成するエンジンに "blorgh" (blogのもじり) という名前を付けます。このエンジンはブログ機能をホストアプリケーションに追加し、記事とコメントを作成できます。本ガイドでは、最初にこのエンジンを単体で動作するようにし、後にこのエンジンをアプリケーションにフックします。

エンジンはホストアプリケーションと混じらないよう分離しておくこともできます。これは、あるアプリケーションがarticles_pathのようなルーティングヘルパーによってパスを提供できるとすると、そのアプリケーションのエンジンも同じくarticles_pathというヘルパーによってパスを提供でき、しかも両者が衝突しないということを意味します。これにともない、コントローラ名、モデル名、テーブル名はいずれも名前空間化されます。これについては本ガイドで後述します。

ここが重要です。アプリケーションは いかなる場合も エンジンよりも優先されます。ある環境において、最終的な決定権を持つのはアプリケーション自身です。エンジンはアプリケーションの動作を大幅に変更するものではなく、アプリケーションを単に拡張するものです。

その他のエンジンに関するドキュメントについては、Devise (親アプリケーションに認証機能を提供するエンジン) や Thredded (フォーラム機能を提供するエンジン) を参照してください。この他に、Spree (eコマースプラットフォーム) やRefinery CMS (CMSエンジン) などもあります。

追伸。エンジン機能はJames Adam、Piotr Sarnacki、Railsコアチーム、そして多くの人々の助けなしではできあがらなかったでしょう。彼らに会うことがあったら、ぜひお礼を述べてやってください。

2 エンジンを生成する

エンジンを生成するには、プラグインジェネレータを実行し、必要に応じてオプションをジェネレータに渡します。"blorgh"の場合はマウント可能なエンジンとして生成するので、ターミナルで以下のコマンドを実行します。

$ bin/rails plugin new blorgh --mountable

プラグインジェネレータで利用できるオプションの一覧をすべて表示するには、以下を入力します。

$ bin/rails plugin --help

--mountableオプションは、マウント可能かつ名前空間で分離されたエンジンを生成する場合に使用します。このジェネレータで生成したプラグインは、--fullオプションを使用した場合と同じスケルトン構造を持ちます。--fullオプションは、以下を提供するスケルトン構造を含むエンジンを作成します。

  • appディレクトリツリー
  • config/routes.rbファイル

    Rails.application.routes.draw do
    end
    
  • lib/blorgh/engine.rbファイルは、Railsアプリケーションが標準で持つconfig/application.rbファイルと同一の機能を持ちます。

    module Blorgh
      class Engine < ::Rails::Engine
      end
    end
    

--mountableオプションを使用すると、--fullオプションによって以下が追加されます。

  • アセットマニフェストファイル (application.jsおよびapplication.css)
  • 名前空間化されたApplicationControllerスタブ
  • 名前空間化されたApplicationHelperスタブ
  • エンジンで使用するレイアウトビューテンプレート
  • config/routes.rbでの名前空間分離

    Blorgh::Engine.routes.draw do
    end
    
  • lib/blorgh/engine.rbでの名前空間分離

    module Blorgh
      class Engine < ::Rails::Engine
        isolate_namespace Blorgh
      end
    end
    

さらに、--mountableオプションはダミーのテスト用アプリケーションを test/dummyに配置するようジェネレータに指示します。これは、以下のダミーアプリケーションのルーティングファイルをtest/dummy/config/routes.rbに追加することによって行います。

mount Blorgh::Engine => "/blorgh"

2.1 エンジンの内部

2.1.1 重要なファイル

新しく作成したエンジンのルートディレクトリには、blorgh.gemspecというファイルが置かれます。アプリケーションにこのエンジンを後からインクルードするには、Gemfileに以下の行を追加します。

gem 'blorgh', path: 'engines/blorgh'

Gemfileを更新したら、いつものようにbundle installを実行するのを忘れずに。エンジンを通常のgemと同様にGemfileに記述すると、Bundlerはgemと同様にエンジンを読み込み、blorgh.gemspecファイルを解析し、lib以下に置かれているファイル (この場合lib/blorgh.rb) をrequireします。このファイルは、(lib/blorgh/engine.rbに置かれている) blorgh/engine.rbファイルをrequireし、Blorghという基本モジュールを定義します。

require "blorgh/engine"

module Blorgh
end

エンジンによっては、このファイルをエンジンのためのグローバル設定オプションとして配置したいこともあるでしょう。これは比較的よいアイディアです。設定オプションを提供したい場合は、エンジンのmoduleが定義されているファイルがまさにこれを行なうのにふさわしい場所と言えます。そのモジュールの中にメソッドを置くことで準備は完了します。

エンジンの基本クラスはlib/blorgh/engine.rbの中にあります。

module Blorgh
  class Engine < ::Rails::Engine
    isolate_namespace Blorgh
  end
end

Rails::Engineクラスを継承することによって、指定されたパスにエンジンがあることがgemからRailsに通知され、アプリケーションの内部にエンジンが正しくマウントされます。そして、エンジンのappディレクトリをモデル/メイラー/コントローラ/ビューの読み込みパスに追加します。

ここで、isolate_namespaceメソッドについて特別な注意が必要です。このメソッドの呼び出しは、エンジンのコントローラ/モデル/ルーティングなどが持つ固有の名前空間を、アプリケーション内部のコンポーネントが持つ類似の名前空間から分離する役目を担います。この呼び出しが行われないと、エンジンのコンポーネントがアプリケーション側に「漏れ出す」リスクが生じ、思わぬ動作が発生したり、エンジンの重要なコンポーネントが同じような名前のアプリケーション側コンポーネントによって上書きされてしまったりする可能性があります。名前の衝突の例として、ヘルパーを取り上げましょう。isolate_namespaceが呼び出されないと、エンジンのヘルパーがアプリケーションのコントローラにインクルードされてしまいます。

Engineクラスの定義に含まれるisolate_namespaceの行を変更/削除しないことを 強く 推奨します。この行が変更されると、生成されたエンジン内のクラスがアプリケーションと衝突する 可能性があります

名前空間を分離するということは、bin/rails g modelの実行によって生成されたモデル (ここでは bin/rails g model articleを実行したとします) はArticleにならず、名前空間化されてBlorgh::Articleになるということです。さらにモデルのテーブルも名前空間化され、単なるarticlesではなくblorgh_articlesになります。コントローラもモデルと同様に名前空間化されます。ArticlesControllerというコントローラはBlorgh::ArticlesControllerになり、このコントローラのビューはapp/views/articlesではなくapp/views/blorgh/articlesに置かれます。メイラーも同様に名前空間化されます。

最後に、ルーティングもエンジン内で分離されます。これは名前空間化の最も肝心な部分であり、これについては本ガイドのルーティングセクションで後述します。

2.1.2 appディレクトリ

エンジンのappディレクトリの中には、通常のアプリケーションでおなじみの標準のassetscontrollershelpersmailersmodelsviewsディレクトリが置かれます。このうちhelpersmailersmodelsディレクトリにはデフォルトでは何も置かれないので、本セクションでは解説しません。モデルについては、エンジンの作成について解説するセクションで後述します。

エンジンのapp/assetsディレクトリの下にも、通常のアプリケーションと同様にimagesjavascriptsstylesheetsディレクトリがそれぞれあります。通常のアプリケーションと異なる点は、これらのディレクトリの下にはさらにエンジン名を持つサブディレクトリがあることです。これは、エンジンが名前空間化されるのと同様、エンジンのアセットも同様に名前空間化される必要があるからです。

app/controllersディレクトリの下にはblorghディレクトリが置かれます。この中にはapplication_controller.rbというファイルが1つ置かれます。このファイルはエンジンのコントローラ共通の機能を提供するためのものです。このblorghディレクトリには、エンジンで使用するその他のコントローラを置きます。これらのファイルを名前空間化されたディレクトリに配置することで、他のエンジンやアプリケーションに同じ名前のコントローラがあっても名前の衝突を避ける事ができます。

あるエンジンに含まれるApplicationControllerというクラスの名前は、アプリケーションそのものが持つクラスと同じ名前になっています。これは、アプリケーションをエンジンに変換しやすくするためです。

Rubyの定数探索方法が原因で、エンジンのコントローラがエンジンのアプリケーションコントローラではなくメインアプリケーションのコントローラを継承してしまう場合があります。RubyがApplicationController定数を解決できる状態になっていると、自動読み込みがトリガされなくなります。詳しくは、定数がトリガーされない場合定数の自動読み込みと再読み込みをご覧ください。この問題を防止するには、require_dependencyを用いてエンジンのアプリケーションコントローラを確実に読み込むのが最善の方法です。次の例をご覧ください。

# app/controllers/blorgh/articles_controller.rb:
require_dependency "blorgh/application_controller"

module Blorgh
  class ArticlesController < ApplicationController
    ...
  end
end

requireは使わないでください。開発環境でのクラス自動読み込みで誤作動の原因になります。require_dependencyを用いることで、クラスの読み込みやunloadを正しい方法で行えるようになります。

最後に、app/viewsディレクトリの下にはlayoutsフォルダがあります。ここにはblorgh/application.html.erbというファイルが置かれます。このファイルは、エンジンで使用するレイアウトを指定するためのものです。エンジンが単体のエンジンとして使用されるのであれば、このファイルを使用していくらでも好きなようにレイアウトをカスタマイズできます。そのためにアプリケーション自身のapp/views/layouts/application.html.erbファイルを変更する必要はありません。

エンジンのレイアウトをユーザーに強制したくない場合は、このファイルを削除し、エンジンのコントローラでは別のレイアウトを参照するように変更してください。

2.1.3 binディレクトリ

このディレクトリにはbin/railsというファイルが1つだけ置かれます。これはアプリケーション内で使用しているのと似たrailsサブコマンドであり、ジェネレータです。このような構成になっていることで、このエンジンで利用するための独自のコントローラやモデルを以下のように簡単に生成することができます。

$ bin/rails g model

言うまでもなく、Engineクラスにisolate_namespaceを持つエンジンでこのbin/railsを使用して生成したものはすべて名前空間化されることにご注意ください。

2.1.4 testディレクトリ

testディレクトリは、エンジンがテストを行なうための場所です。エンジンをテストするために、test/dummyディレクトリに埋め込まれた縮小版のRailsアプリケーションが用意されます。このアプリケーションはエンジンをtest/dummy/config/routes.rbファイル内で以下のようにマウントします。

Rails.application.routes.draw do
  mount Blorgh::Engine => "/blorgh"
end

上の行によって、/blorghパスにあるエンジンがマウントされ、アプリケーションのこのパスを通じてのみアクセス可能になります。

testディレクトリの下にはtest/integrationディレクトリがあります。ここにはエンジンの結合テストが置かれます。testディレクトリに他のディレクトリを作成することもできます。たとえば、モデルのテスト用にtest/modelsディレクトリを作成しても構いません。

3 エンジンの機能を提供する

本ガイドで説明のために作成するエンジンには、記事とコメントの送信機能があります。基本的にはRailsをはじめようとよく似たスレッドに従いますが、多少の新味も加えられています。

3.1 Articleリソースを生成する

ブログエンジンで最初に生成すべきはArticleモデルとそれに関連するコントローラです。これらを手軽に生成するために、Railsのscaffoldジェネレータを使用します。

$ bin/rails generate scaffold article title:string text:text

上のコマンドを実行すると以下の情報が出力されます。

invoke  active_record
create    db/migrate/[timestamp]_create_blorgh_articles.rb
create    app/models/blorgh/article.rb
invoke  test_unit
create      test/models/blorgh/article_test.rb
create      test/fixtures/blorgh/articles.yml
invoke  resource_route
route    resources :articles
invoke  scaffold_controller
create    app/controllers/blorgh/articles_controller.rb
invoke    erb
create      app/views/blorgh/articles
create      app/views/blorgh/articles/index.html.erb
create      app/views/blorgh/articles/edit.html.erb
create      app/views/blorgh/articles/show.html.erb
create      app/views/blorgh/articles/new.html.erb
create      app/views/blorgh/articles/_form.html.erb
invoke  test_unit
create      test/controllers/blorgh/articles_controller_test.rb
invoke    helper
create      app/helpers/blorgh/articles_helper.rb
invoke  test_unit
create    test/application_system_test_case.rb
create    test/system/articles_test.rb
invoke  assets
invoke    js
create      app/assets/javascripts/blorgh/articles.js
invoke    css
create      app/assets/stylesheets/blorgh/articles.css
invoke  css
create    app/assets/stylesheets/scaffold.css

scaffoldジェネレータが最初に行なうのはactive_recordジェネレータの呼び出しです。これはマイグレーションの生成とそのリソースのモデルを生成します。ここでご注目いただきたいのは、マイグレーションは通常のcreate_articlesではなくcreate_blorgh_articlesという名前で呼ばれるという点です。これはBlorgh::Engineクラスの定義で呼び出されるisolate_namespaceメソッドによるものです。このモデルも名前空間化されるので、Engineクラス内のisolate_namespace呼び出しによって、app/models/article.rbではなくapp/models/blorgh/article.rbに置かれます。

続いて、そのモデルに対応するtest_unitジェネレータが呼び出され、(test/models/article_test.rbではなく) test/models/blorgh/article_test.rb にモデルのテストが置かれます。フィクスチャも同様に (test/fixtures/articles.ymlではなく) test/fixtures/blorgh/articles.ymlに置かれます。

その後、そのリソースに対応する行がconfig/routes.rbファイルに挿入され、エンジンで使用されます。ここで挿入される行は単にresources :articlesとなっています。これにより、そのエンジンで使用するconfig/routes.rbファイルが以下のように変更されます。

Blorgh::Engine.routes.draw do
  resources :articles
end

このルーティングは、YourApp::ApplicationクラスではなくBlorgh::Engineオブジェクトにもとづいていることにご注目ください。これにより、エンジンのルーティングがエンジン自身に制限され、testディレクトリセクションで説明したように特定の位置にマウントできるようになります。ここでは、エンジンのルーティングがアプリケーション内のルーティングから分離されていることにもご注目ください。詳細については本ガイドのルーティングセクションで解説します。

続いてscaffold_controllerジェネレータが呼ばれ、Blorgh::ArticlesControllerという名前のコントローラを生成します (生成場所はapp/controllers/blorgh/articles_controller.rbです)。このコントローラに関連するビューはapp/views/blorgh/articlesとなります。このジェネレータは、コントローラ用のテスト (test/controllers/blorgh/articles_controller_test.rb) とヘルパー (app/helpers/blorgh/articles_helper.rb) も同時に生成します。

このジェネレータによって生成されるものはすべて正しく名前空間化されます。このコントローラのクラスは、以下のようにBlorghモジュール内で定義されます。

module Blorgh
  class ArticlesController < ApplicationController
    ...
  end
end

このクラスで継承されているArticlesControllerクラスは、実際にはApplicationControllerではなく、Blorgh::ApplicationControllerです。

app/helpers/blorgh/articles_helper.rbのヘルパーも同様に名前空間化されます。

module Blorgh
  module ArticlesHelper
    ...
  end
end

これにより、たとえ他のエンジンやアプリケーションにarticleリソースがあっても衝突を回避できます。

最後に、以下の2つのファイルがこのリソースのアセットとして生成されます。 app/assets/javascripts/blorgh/articles.jsapp/assets/stylesheets/blorgh/articles.cssです。これらの使用法についてはこのすぐ後で解説します。

エンジンのルートディレクトリでbin/rails db:migrateを実行すると、scaffoldジェネレータによって生成されたマイグレーションが実行されます。続いてtest/dummyディレクトリでrails serverを実行してみましょう。http://localhost:3000/blorgh/articlesをブラウザで表示すると、生成されたデフォルトのscaffoldが表示されます。表示されたものをいろいろクリックしてみてください。これで、最初の機能を備えたエンジンの生成に成功しました。

コンソールで遊んでみたいのであれば、rails consoleでRailsアプリケーションをコンソールで動かせます。先ほどから申し上げているように、Articleモデルは名前空間化されていますので、このモデルを参照する際にはBlorgh::Articleと指定する必要があります。

>> Blorgh::Article.find(1)
=> #<Blorgh::Article id: 1 ...>

最後の作業です。このエンジンのarticlesリソースはエンジンのルート (root) パスに置くのがふさわしいでしょう。エンジンがマウントされているルートパスに移動したら、記事の一覧が表示されるようにしたいものです。エンジンにあるconfig/routes.rbファイルに以下の記述を追加することでこれを実現できます。

root to: "articles#index"

これで、ユーザーが (/articlesではなく) エンジンのルートパスに移動すると記事の一覧が表示されるようになりました。つまり、http://localhost:3000/blorgh/articlesに移動しなくてもhttp://localhost:3000/blorghに移動すれば済むということです。

3.2 commentsリソースを生成する

エンジンで記事を新規作成できるようになりましたので、今度は記事にコメントを追加する機能も付けてみましょう。これを行なうには、commentモデルとcommentsコントローラを生成し、articles scaffoldを変更してコメントを表示できるようにし、それから新規コメントを作成できるようにします。

アプリケーションのルート・ディレクトリで、モデルのジェネレータを実行します。このとき、Commentモデルを生成すること、integer型のarticle_idカラムとtext型のtextカラムを持つテーブルと関連付けることを指示します。

$ bin/rails generate model Comment article_id:integer text:text

上によって以下が出力されます。

invoke  active_record
create    db/migrate/[timestamp]_create_blorgh_comments.rb
create    app/models/blorgh/comment.rb
invoke    test_unit
create      test/models/blorgh/comment_test.rb
create      test/fixtures/blorgh/comments.yml

このジェネレータ呼び出しでは必要なモデルファイルだけが生成されます。さらにblorghディレクトリの下で名前空間化され、Blorgh::Commentというモデルクラスも作成されます。それではマイグレーションを実行してblorgh_commentsテーブルを生成してみましょう。

$ bin/rails db:migrate

記事のコメントを表示できるようにするために、app/views/blorgh/articles/show.html.erbを編集して以下の行を"Edit"リンクの直前に追加します。

<h3>Comments</h3>
<%= render @article.comments %>

上の行では、Blorgh::Articleモデルとコメントがhas_many関連付けとして定義されている必要がありますが、現時点ではまだありません。この定義を行なうために、app/models/blorgh/article.rbを開いてモデルに以下の行を追加します。

has_many :comments

これにより、モデルは以下のようになります。

module Blorgh
  class Article < ApplicationRecord
    has_many :comments
  end
end

このhas_manyBlorghモジュールの中にあるクラスの中で定義されています。これだけで、これらのオブジェクトに対してBlorgh::Commentモデルを使用したいという意図がRailsに自動的に認識されます。従って、ここで:class_nameオプションを使用してクラス名を指定する必要はありません。

続いて、記事を作成するためのフォームを作成する必要があります。フォームを追加するには、app/views/blorgh/articles/show.html.erbrender @article.comments呼び出しの直後に以下の行を追加します。

<%= render "blorgh/comments/form" %>

続いて、この行を出力に含めるためのパーシャル (部分テンプレート) も必要です。app/views/blorgh/commentsにディレクトリを作成し、_form.html.erbというファイルを作成します。このファイルの中に以下のパーシャルを記述します。

<h3>New comment</h3>
<%= form_with(model: [@article, @article.comments.build], local: true) do |form| %>
  <p>
    <%= form.label :text %><br>
    <%= form.text_area :text %>
  </p>
  <%= form.submit %>
<% end %>

このフォームが送信されると、エンジン内の/articles/:article_id/commentsというルーティングに対してPOSTリクエストを送信しようとします。このルーティングはまだ存在していませんので、config/routes.rbresources :articles行を以下のように変更します。

resources :articles do
  resources :comments
end

これでcomments用のネストしたルーティングが作成されました。これが上のフォームで必要となります。

ルーティングは作成しましたが、ルーティング先のコントローラがまだありません。これを作成するには、アプリケーションのルート・ディレクトリで以下のコマンドを実行します。

$ bin/rails g controller comments

上によって以下が生成されます。

create  app/controllers/blorgh/comments_controller.rb
invoke  erb
exist    app/views/blorgh/comments
invoke  test_unit
create    test/controllers/blorgh/comments_controller_test.rb
invoke  helper
create    app/helpers/blorgh/comments_helper.rb
invoke  assets
invoke    js
create      app/assets/javascripts/blorgh/comments.js
invoke    css
create      app/assets/stylesheets/blorgh/comments.css

このフォームはPOSTリクエストを/articles/:article_id/commentsに送信します。これに対応するのはBlorgh::CommentsControllercreateアクションです。このアクションを作成する必要があります。app/controllers/blorgh/comments_controller.rbのクラス定義の中に以下の行を追加します。

def create
  @article = Article.find(params[:article_id])
  @comment = @article.comments.create(comment_params)
  flash[:notice] = "Comment has been created!"
  redirect_to articles_path
end

private
  def comment_params
    params.require(:comment).permit(:text)
  end

いよいよ、コメントフォームが動作するのに必要な最後の手順を行いましょう。コメントはまだ正常に表示できません。この時点でコメントを作成しようとすると、以下のようなエラーが生じるでしょう。

Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder],
:formats=>[:html], :locale=>[:en, :en]}. Searched in:   *
"/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views"   *
"/Users/ryan/Sites/side_projects/blorgh/app/views"

このエラーは、コメントの表示に必要なパーシャルが見つからないためです。Railsはアプリケーションの (test/dummy) app/viewsを最初に検索し、続いてエンジンのapp/viewsディレクトリを検索します。見つからない場合はエラーになります。エンジン自身はblorgh/comments/commentを検索すべきであることを認識しています。これは、エンジンが受け取るモデルオブジェクトがBlorgh::Commentクラスに属しているためです。

さしあたって、コメントテキストを出力する役目をこのパーシャルに担ってもらわなければなりません。app/views/blorgh/comments/_comment.html.erbファイルを作成し、以下の記述を追加します。

<%= comment_counter + 1 %>. <%= comment.text %>

<%= render @article.comments %>呼び出しによってcomment_counterローカル変数が返されます。この変数は自動的に定義され、コメントをiterateするたびにカウントアップします。この例では、作成されたコメントの横に小さな数字を表示するのに使用しています。

これでブログエンジンのコメント機能ができました。今度はこの機能をアプリケーションの中で使用してみましょう。

4 アプリケーションにフックする

エンジンをアプリケーションで利用するのはきわめて簡単です。本セクションでは、エンジンをアプリケーションにマウントして必要な初期設定を行い、アプリケーションが提供するUserクラスにエンジンをリンクして、エンジン内の記事とコメントに所有者を与えるところまでをカバーします。

4.1 エンジンをマウントする

最初に、使用するエンジンをアプリケーションのGemfileに記述する必要があります。テストに使用できる手頃なアプリケーションが見当たらない場合は、エンジンのディレクトリの外で以下のrails newコマンドを実行してアプリケーションを作成してください。

$ rails new unicorn

基本的には、Gemfileでエンジンを指定する方法は他のgemの指定方法と変わりません。

gem 'devise'

ただし、このblorghエンジンはローカルPCで開発中でgemリポジトリには存在しないので、Gemfileでエンジンgemへのパスを:pathオプションで指定する必要があります。

gem 'blorgh', path: 'engines/blorgh'

続いてbundleコマンドを実行し、gemをインストールします。

前述したように、Gemfileに記述したgemはRailsの読み込み時に読み込まれます。このgemは最初にエンジンのlib/blorgh.rbをrequireし、続いてlib/blorgh/engine.rbをrequireします。後者はこのエンジンの機能を担う主要な部品が定義されている場所です。

アプリケーションからエンジンの機能にアクセスできるようにするには、エンジンをアプリケーションのconfig/routes.rbファイルでマウントする必要があります。

mount Blorgh::Engine, at: "/blog"

この行を記述することで、エンジンがアプリケーションの/blogパスにマウントされます。rails serverを実行してRailsを起動すると、http://localhost:3000/blogにアクセスできるようになります。

Deviseなどの他のエンジンではこの点が若干異なり、ルーティングで (devise_forなどの) カスタムヘルパーを指定するものがあります。これらのヘルパーの動作は完全に同じです。事前に定義されたカスタマイズ可能なパスにエンジンの機能の一部をマウントします。

4.2 エンジンの設定

作成したエンジンにはblorgh_articlesテーブルとblorgh_commentsテーブル用のマイグレーションが含まれます。これらのテーブルをアプリケーションのデータベースに作成し、エンジンのモデルからこれらのテーブルにアクセスできるようにする必要があります。これらのマイグレーションをアプリケーションにコピーするには、自分のRailsエンジンのtest/dummyディレクトリで以下のコマンドを実行します。

$ bin/rails blorgh:install:migrations

マイグレーションをコピーする必要のあるエンジンがいくつもある場合は、代りにrailties:install:migrationsを使用します。

$ bin/rails railties:install:migrations

このコマンドは、初回実行時にエンジンからすべてのマイグレーションをコピーします。次回以降の実行時には、コピーされていないマイグレーションのみがコピーされます。このコマンドの初回実行時の出力結果は以下のようになります。

Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh

最初のタイムスタンプ ([timestamp_1]) が現在時刻、次のタイムスタンプ ([timestamp_2]) が現在時刻に1秒追加した値になります。このようになっているのは、エンジンのマイグレーションはアプリケーションの既存のマイグレーションがすべて終わってから実行する必要があるためです。

アプリケーションのコンテキストでマイグレーションを実行するには、単にbin/rails db:migirateを実行します。http://localhost:3000/blogでエンジンにアクセスすると、記事は空の状態です。これは、アプリケーションの内部で作成されたテーブルはエンジンの内部で作成されたテーブルとは異なるためです。新しくマウントしたエンジンでもっといろいろやってみましょう。アプリケーションの動作は、エンジンを単体で動かしているときと同じであることに気付くことでしょう。

エンジンを1つだけマイグレーションしたい場合、以下のようにSCOPEを指定します。

bin/rails db:migrate SCOPE=blorgh

このオプションは、エンジンを削除する前にマイグレーションを元に戻したい場合などに便利です。blorghエンジンによるすべてのマイグレーションを元に戻したい場合は以下のようなコマンドを実行します。

bin/rails db:migrate SCOPE=blorgh VERSION=0

4.3 アプリケーションが提供するクラスを使用する

4.3.1 アプリケーションが提供するモデルを使用する

エンジンをひとつ作成すると、やがてエンジンの部品とアプリケーションの部品を連携させるために、アプリケーションの特定のクラスをエンジンから利用したくなるでしょう。このblorghエンジンであれば、記事とコメントの作者の情報がある方がずっとわかりやすくなります。

普通のアプリケーションであれば、記事やコメントの作者を表すためのUserクラスが備わっているでしょう。しかしクラス名がUserとは限りません。アプリケーションによってはPersonというクラスであるかもしれません。このような状況に対応するために、このエンジンではUserクラスとの関連付けをハードコードしないようにすべきです。

ここでは話を簡単にするため、アプリケーションがユーザーを表すために持つクラスはUserであるとします (この後でもっとカスタマイズしやすくします)。このクラスは、アプリケーションで以下のコマンドを実行して生成できます。

rails g model user name:string

今後usersテーブルをアプリケーションで使用できるようにするために、ここでbin/rails db:migrateを実行する必要があります。

話を簡単にするため、記事のフォームのテキストフィールドはauthor_nameとすることにします。記事を書くユーザーがここに自分の名前を入れられるようにします。エンジンはこの名前を使用してUserオブジェクトを新規作成するか、その名前が既にあるかどうかを調べます。続いて、エンジンは作成または見つけたUserオブジェクトを記事と関連付けます。

最初に、author_nameテキストフィールドをエンジンのパーシャルapp/views/blorgh/articles/_form.html.erbに追加する必要があります。そこで、以下のコードをtitleフィールドのすぐ上に追加します。

<div class="field">
  <%= form.label :author_name %><br>
  <%= form.text_field :author_name %>
</div>

続いて、エンジンのBlorgh::ArticleController#article_paramsメソッドを更新して、新しいフォームパラメータを受け付けるようにする必要もあります。

def article_params
  params.require(:article).permit(:title, :text, :author_name)
end

次に、Blorgh::Articleモデルにもauthor_nameフィールドを実際のUserオブジェクトに変換し、Userオブジェクトを記事のauthorと関連付けてから記事を保存するコードが必要です。このフィールド用のattr_accessorも設定する必要があります。これにより、このフィールド用のゲッターとセッターが定義されます。

これらをすべて行なうには、author_name用のattr_accessorと、authorとの関連付け、およびbefore_validation呼び出しをapp/models/blorgh/article.rbに追加する必要があります。author関連付けは、この時点ではあえてUserクラスとハードコードしておきます。

attr_accessor :author_name
belongs_to :author, class_name: "User"

before_validation :set_author

private
  def set_author
    self.author = User.find_or_create_by(name: author_name)
  end

authorオブジェクトとUserクラスの関連付けを示すことにより、エンジンとアプリケーションの間にリンクが確立されます。blorgh_articlesテーブルのレコードと、usersテーブルのレコードを関連付けるための方法が必要です。この関連付けはauthorという名前なので、blorgh_articlesテーブルにはauthor_idというカラムが追加される必要があります。

この新しいカラムを追加するには、エンジンのディレクトリで以下のコマンドを実行する必要があります。

$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer

上のようにコマンドオプションでマイグレーション名とカラムの仕様を指定することで、特定のテーブルに追加しようとしているカラムがRailsによって自動的に認識され、そのためのマイグレーションが作成されます。この他にオプションを指定する必要はありません。

このマイグレーションはアプリケーションに対して実行する必要があります。これを行なうには、最初に以下のコマンドを実行してマイグレーションをエンジンからコピーする必要があります。

$ bin/rails blorgh:install:migrations

上のコマンドでコピーされるマイグレーションは 1つ だけである点にご注意ください。これは、最初の2つのマイグレーションはこのコマンドが初めて実行されたときにコピー済みであるためです。

NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.
NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.
Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh

このマイグレーションを実行するコマンドは以下のとおりです。

$ bin/rails db:migrate

これですべての部品が定位置に置かれ、ある記事 (article) を、usersテーブルのレコードで表される作者 (author) に関連付けるアクションが実行されるようになりました。この記事はblorgh_articlesテーブルで表されます。

最後に、作者名を記事のページに表示しましょう。以下のコードをapp/views/blorgh/articles/show.html.erbの"Title"出力の上に追加します。

<p>
  <b>Author:</b>
  <%= @article.author.name %>
</p>
4.3.2 アプリケーションのコントローラを使用する

Railsのコントローラでは、認証やセッション変数へのアクセスに関するコードをアプリケーション全体で共有するのが一般的です。従って、このようなコードはデフォルトでApplicationControllerから継承します。しかし、Railsのエンジンは基本的にメインとなるアプリケーションから独立しているので、エンジンが利用できるApplicationControllerはスコープで制限されています。名前空間が導入されていることでコードの衝突は回避されますが、エンジンのコントローラからメインアプリケーションのApplicationControllerのメソッドにアクセスする必要も頻繁に発生します。エンジンのコントローラからメインアプリケーションのApplicationControllerへのアクセスを提供するには、エンジンが所有するスコープ付きのApplicationControllerに変更を加え、メインアプリケーションのApplicationControllerを継承するのが簡単な方法です。Blorghエンジンの場合、app/controllers/blorgh/application_controller.rbを以下のように変更します。

module Blorgh
  class ApplicationController < ::ApplicationController
  end
end

エンジンのコントローラはデフォルトでBlorgh::ApplicationControllerを継承します。上の変更を行なうことで、あたかもエンジンがアプリケーションの一部であるかのように、エンジンのコントローラでApplicationControllerにアクセスできるようになります。

この変更を行なうには、エンジンをホストするRailsアプリケーションにApplicationControllerという名前のコントローラが存在する必要があります。

4.4 エンジンを設定する

このセクションでは、Userクラスをカスタマイズ可能にする方法を解説し、続いてエンジンの一般的な設定方法について解説します。

4.4.1 アプリケーションの設定を行なう

これより、アプリケーションでUserを表すクラスをエンジンからカスタマイズ可能にする方法について説明します。カスタマイズしたいクラスは、前述のUserのようなクラスばかりとは限りません。このクラスの設定をカスタマイズ可能にするには、エンジン内部にauthor_classという名前の設定が必要です。この設定は、親アプリケーション内部でユーザーを表すクラスがどれであるかを指定するためのものです。

この設定を定義するには、エンジンで使用するBlorghモジュール内部にmattr_accessorというアクセッサを置く必要があります。エンジンにあるlib/blorgh.rbに以下の行を追加します。

mattr_accessor :author_class

このメソッドの動作はattr_accessorcattr_accessorなどの兄弟メソッドと似ていますが、モジュールのゲッター名とセッター名に指定された名前を使用します。これらを使用する場合はBlorgh.author_classという名前で参照する必要があります。

続いて、Blorgh::Articleモデルの設定をこの新しい設定に切り替えます。app/models/blorgh/article.rbモデル内のbelongs_to関連付けを以下のように変更します。

belongs_to :author, class_name: Blorgh.author_class

Blorgh::Articleモデルのset_authorメソッドは以下のクラスも使用する必要があります。

self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)

author_classで保存時にconstantizeが必ず呼び出されるようにしたい場合は、lib/blorgh.rbBlorghモジュール内部のauthor_classゲッターメソッドをオーバーライドするだけでできます。これにより、値の保存時に必ずconstantizeを呼び出してから結果が返されます。

def self.author_class
  @@author_class.constantize
end

これにより、set_author用の上のコードは以下のようになります。

self.author = Blorgh.author_class.find_or_create_by(name: author_name)

これにより、記述がやや短くなり、動作がやや明示的でなくなります。このauthor_classメソッドは常にClassオブジェクトを返す必要があります。

author_classメソッドがStringではなくClassを返すように変更したので、Blorgh::Articlebelongs_to定義もそれに合わせて変更する必要があります。

belongs_to :author, class_name: Blorgh.author_class.to_s

この設定をアプリケーション内で行なうには、イニシャライザを使用する必要があります。イニシャライザを使用することで、アプリケーションの設定はアプリケーションが起動してエンジンのモデルを呼び出すまでに完了します。この動作は既存のこの設定に依存する場合があります。

blorghがインストールされているアプリケーションのconfig/initializers/blorgh.rbにイニシャライザを作成して、以下の記述を追加します。

Blorgh.author_class = "User"

このクラス名は必ずStringで (=引用符で囲んで) 表してください。クラス自身を使用しないでください。クラス自身が使用されていると、Railsはそのクラスを読み込んで関連するテーブルを参照しようとします。このとき参照先のテーブルが存在しないと問題が発生する可能性があります。このため、クラス名はStringで表し、後にエンジンがconstantizeでクラスに変換する必要があります。

続いて、新しい記事を1つ作成してみることにしましょう。記事の作成はこれまでとまったく同様に行えます。1つだけ異なるのは、今回はクラスの動作を学ぶためにconfig/initializers/blorgh.rbの設定をエンジンで使用する点です。

使用するクラスがそのためのAPIさえ備えていれば、使用するクラスに厳密に依存することはありません。エンジンで使用するクラスで必須となるメソッドはfind_or_create_byのみです。このメソッドはそのクラスのオブジェクトを1つ返します。もちろん、このオブジェクトは何らかの形で参照可能な識別子 (id) を持つ必要があります。

4.4.2 一般的なエンジンの設定

エンジンを使ううちに、その中でイニシャライザや国際化などの機能オプションを使用したくなることでしょう。うれしいことに、RailsエンジンはRailsアプリケーションと大半の機能を共有しているので、これらは完全に実現可能です。実際、Railsアプリケーションが持つ機能はエンジンが持つ機能のスーパーセットなのです。

たとえばイニシャライザ (エンジンが読み込まれる前に実行されるコード) を使用したいのであれば、そのための場所であるconfig/initializersフォルダに置きます。このディレクトリの機能については『Railsアプリケーションを設定する』ガイドのイニシャライザファイルを使用するを参照してください。エンジンのイニシャライザは、アプリケーションのconfig/initializersディレクトリに置かれているイニシャライザとまったく同様に動作します。標準のイニシャライザを使用したい場合も同様です。

ロケールファイルも、アプリケーションの場合と同様config/localesディレクトリに置けばよいようになっています。

5 エンジンをテストする

エンジンが生成されると、test/dummyディレクトリの下に小規模なダミーアプリケーションが自動的に配置されます。このダミーアプリケーションはエンジンのマウント場所として使用されるので、エンジンのテストがきわめてシンプルになります。このディレクトリ内でコントローラやモデル、ビューを生成してアプリケーションを拡張し、続いてこれらを使用してエンジンをテストできます。

testディレクトリは、通常のRailsにおけるtesting環境と同様に扱う必要があります。Railsのtesting環境では単体テスト、機能テスト、結合テストを行なうことができます。

5.1 機能テスト

特に機能テストを作成する際には、テストが実行されるのはエンジンではなくtest/dummyに置かれるダミーアプリケーション上であるという点に留意する必要があります。このようになっているのは、testing環境がそのように設定されているためです。エンジンの主要な機能、特にコントローラをテストするには、エンジンをホストするアプリケーションが必要です。これはコントローラの機能テストの中で一般的なGETをテストするためには以下のようにする必要があるという意味です。

module Blorgh
  class FooControllerTest < ActionDispatch::IntegrationTest
    include Engine.routes.url_helpers

    def test_index
      get foos_url
      ...
    end
  end
end

しかしこれは正常に機能しないでしょう。アプリケーションは、このようなリクエストをエンジンにルーティングする方法を知らないので、明示的にエンジンにルーティングする必要があります。これを行なうには、設定コードの中で@routesインスタンス変数にエンジンのルーティングを割り当てる必要があります。

module Blorgh
  class FooControllerTest < ActionDispatch::IntegrationTest
    include Engine.routes.url_helpers

    setup do
      @routes = Engine.routes
    end

    def test_index
      get foos_url
      ...
    end
  end
end

上のようにすることで、このコントローラのindexアクションに対してGETリクエストを送信しようとしていることがアプリケーションによって認識され、かつそのためにアプリケーションのルーティングではなくエンジンのルーティングが使用されるようになります。

こうすることで、エンジン用のURLヘルパーもテストで期待どおりに動作します。

6 エンジンの機能を改良する

このセクションでは、エンジンのMVC機能をメインのRailsアプリケーションに追加またはオーバーライドする方法について解説します。

6.1 モデルとコントローラをオーバーライドする

エンジンのモデルクラスとコントローラクラスは、オープンクラスとしてメインのRailsアプリケーションで拡張可能です。Railsのモデルクラスとコントローラクラスは、Rails特有の機能を継承しているほかは通常のRubyクラスと変わりありません。エンジンのクラスをオープンクラス化 (open classing) することで、メインのアプリケーションで使用できるように再定義されます。これは、デザインパターンで言うdecoratorパターンとして実装するのが普通です。

クラスの変更内容が単純であれば、Class#class_evalを使用します。クラスの変更が複雑な場合は、ActiveSupport::Concernの使用をご検討ください。

6.1.1 デコレータとコードの読み込みに関するメモ

Railsアプリケーション自身はこれらのデコレータを参照することはないので、Railsの自動読み込み機能ではこれらのデコレータを読み込んだり起動したりできません。つまり、デコレータは手動でrequireする必要があるということです。

これを行なうためのサンプルコードをいくつか掲載します。

# lib/blorgh/engine.rb
module Blorgh
  class Engine < ::Rails::Engine
    isolate_namespace Blorgh

    config.to_prepare do
      Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
        require_dependency(c)
      end
    end
  end
end

上のコードは、デコレータだけではなく、メインのアプリケーションから参照されないすべてのエンジンのコードを読み込みます。

6.1.2 Class#class_evalを使用してdecoratorパターンを実装する

Article#time_since_created追加する場合:

# MyApp/app/decorators/models/blorgh/article_decorator.rb

Blorgh::Article.class_eval do
  def time_since_created
    Time.current - created_at
  end
end
# Blorgh/app/models/article.rb

class Article < ApplicationRecord
  has_many :comments
end

Article#summaryオーバーライドする場合:

# MyApp/app/decorators/models/blorgh/article_decorator.rb

Blorgh::Article.class_eval do
  def summary
    "#{title} - #{truncate(text)}"
  end
end
# Blorgh/app/models/article.rb

class Article < ApplicationRecord
  has_many :comments
  def summary
    "#{title}"
  end
end
6.1.3 ActiveSupport::Concernを使用してdecoratorパターンを実装する

Class#class_evalは単純な調整には大変便利ですが、クラスの変更が複雑になるのであればActiveSupport::Concernをご検討ください。ActiveSupport::Concernは、相互にリンクしている依存モジュールおよび依存クラスの実行時読み込み順序を管理し、コードのモジュール化を高めます。

Article#time_since_created追加してArticle#summaryオーバーライドする場合:

# MyApp/app/models/blorgh/article.rb

class Blorgh::Article < ApplicationRecord
  include Blorgh::Concerns::Models::Article

  def time_since_created
    Time.current - created_at
  end

  def summary
    "#{title} - #{truncate(text)}"
  end
end
# Blorgh/app/models/article.rb

class Article < ApplicationRecord
  include Blorgh::Concerns::Models::Article
end
# Blorgh/lib/concerns/models/article.rb

module Blorgh::Concerns::Models::Article
  extend ActiveSupport::Concern

  # 'included do'は、インクルードされたコードを
  # それがインクルードされている (article.rb) コンテキストで評価する
  # そのモジュールのコンテキストで実行されている (blorgh/concerns/models/article) は評価しない
  included do
    attr_accessor :author_name
    belongs_to :author, class_name: "User"

    before_validation :set_author

    private
      def set_author
        self.author = User.find_or_create_by(name: author_name)
      end
  end

  def summary
    "#{title}"
  end

  module ClassMethods
    def some_class_method
      'some class method string'
    end
  end
end

6.2 ビューをオーバーライドする

Railsは出力すべきビューを探索する際に、アプリケーションのapp/viewsディレクトリを最初に探索します。探しているビューがそこにない場合、続いてそのディレクトリを持つすべてのエンジンのapp/viewsディレクトリを探索します。

たとえば、アプリケーションがBlorgh::ArticlesControllerのindexアクションの結果を出力するためのビューを探索する際には、最初にアプリケーション自身のapp/views/blorgh/articles/index.html.erbを探索します。そこに見つからない場合は、続いてエンジンの中を探索します。

app/views/blorgh/articles/index.html.erbというファイルを作成することで、上の動作を上書きすることができます。こうすることで、通常のビューでの出力結果を完全に変えることができます。

app/views/blorgh/articles/index.html.erbというファイルを作成して以下のコードを追加するとします。

<h1>Articles</h1>
<%= link_to "New Article", new_article_path %>
<% @articles.each do |article| %>
  <h2><%= article.title %></h2>
  <small>By <%= article.author %></small>
  <%= simple_format(article.text) %>
  <hr>
<% end %>

6.3 ルーティング

デフォルトでは、エンジン内部のルーティングはアプリケーションのルーティングから分離されています。これは、Engineクラス内のisolate_namespace呼び出しによって実現されます。これは本質的に、アプリケーションとエンジンが完全に同一の名前のルーティングを持つことができ、しかも衝突しないということを意味します。

エンジン内部のルーティングは、以下のようにconfig/routes.rbEngineクラスによって構成されます。

Blorgh::Engine.routes.draw do
  resources :articles
end

エンジンとアプリケーションのルーティングがこのように分離されているので、アプリケーションの特定の部分をエンジンの特定の部分にリンクしたい場合は、エンジンのルーティングプロキシメソッドを使用する必要があります。articles_pathのような通常のルーティングメソッドの呼び出しは、アプリケーションとエンジンの両方でそのようなヘルパーが定義されている場合には期待と異なる場所にリンクされる可能性があります。

たとえば以下のコード例では、そのテンプレートがアプリケーションでレンダリングされる場合の行き先はアプリケーションのarticles_pathになり、エンジンでレンダリングされる場合の行き先はエンジンのarticles_pathになります。

<%= link_to "Blog articles", articles_path %>

このルーティングを常にエンジンのarticles_pathルーティングヘルパーメソッドで取り扱うようにしたい場合、以下のようにエンジンと同じ名前を共有するルーティングプロキシメソッドを呼び出す必要があります。

<%= link_to "Blog articles", blorgh.articles_path %>

逆にエンジン内部からアプリケーションを参照する場合は、同じ要領でmain_appを使用します。

<%= link_to "Home", main_app.root_path %>

上のコードをエンジン内で使用すると、行き先は常にアプリケーションのルートになります。このmain_appルーティングプロキシメソッドを呼び出しを省略すると、行き先は呼び出された場所によってアプリケーションまたはエンジンのいずれかとなって確定しません。

ルーティングプロキシメソッド呼び出しを省略したこのようなアプリケーションルーティングヘルパーメソッドを、エンジン内でレンダリングされるテンプレートから呼び出そうとすると、未定義メソッド呼び出しエラーが発生することがあります。このような問題が発生した場合は、アプリケーションのルーティングメソッドを、main_appというプレフィックスを付けずにエンジンから呼びだそうとしていないかどうかを確認してください。

6.4 アセット

エンジンのアセットは、通常のアプリケーションで使用されるアセットとまったく同じように機能します。エンジンのクラスはRails::Engineを継承しているので、アプリケーションはエンジンのapp/assetsディレクトリとlib/assetsディレクトリを探索対象として認識します。

エンジン内の他のコンポーネントと同様、アセットも名前空間化される必要があります。たとえば、style.cssというアセットは、app/assets/stylesheets/style.cssではなくapp/assets/stylesheets/[エンジン名]/style.cssに置かれる必要があります。アセットが名前空間化されないと、ホストアプリケーションに同じ名前のアセットが存在する場合にアプリケーションのアセットが使用されてエンジンのアセットが使用されないということが発生する可能性があります。

app/assets/stylesheets/blorgh/style.cssというアセットを例にとって説明します。このアセットをアプリケーションに含めるには、stylesheet_link_tagを使用してアセットがあたかもエンジン内部にあるかのように参照します。

<%= stylesheet_link_tag "blorgh/style.css" %>

処理されるファイルでアセットパイプラインのrequireステートメントを使用して、これらのアセットが他のアセットに依存することを指定することもできます。

/*
*= require blorgh/style
*/

SassやCoffeeScriptなどの言語を使用する場合は、必要なライブラリを.gemspecに追加する必要があります。

6.5 アセットとプリコンパイルを分離する

エンジンが持つアセットは、ホスト側のアプリケーションでは必ずしも必要ではないことがあります。たとえば、エンジンでしか使用しない管理機能を作成したとしましょう。この場合、ホストアプリケーションではadmin.cssadmin.jsは不要です。これらのアセットを必要とするのは、gemのadminレイアウトしかないからです。ホストアプリケーションから見れば、自分が持つスタイルシートに"blorgh/admin.css"を追加する意味はありません。このような場合、これらのアセットを明示的にプリコンパイルする必要があります。それにより、bin/rails assets:precompileが実行されたときにエンジンのアセットを追加するようSprocketsに指示されます。

プリコンパイルの対象となるアセットはengine.rbで定義できます。

initializer "blorgh.assets.precompile" do |app|
  app.config.assets.precompile += %w( admin.js admin.css )
end

詳細については、アセットパイプラインガイドを参照してください。

6.6 他のgemとの依存関係

エンジンが依存するgemについては、エンジンのルートディレクトリの.gemspecに記述する必要があります。エンジンはgemとしてインストールされるので、このようにする必要があります。依存関係をGemfileに指定したのでは伝統的なgemインストールで依存関係が認識されないので、必要なgemが自動的にインストールされず、エンジンが正常に機能しなくなります。

伝統的なgem installコマンド実行時に同時にインストールされる必要のあるgemを指定するには、以下のようにエンジンの.gemspecファイルにあるGem::Specificationブロックの内側に記述します。

s.add_dependency "moo"

アプリケーションの開発時にのみ必要となるgemのインストールを指定するには、以下のように記述します。

s.add_development_dependency "moo"

どちらの依存gemも、アプリケーションでbundle installを実行するときにインストールされます。開発時にのみ必要となるgemは、エンジンのテスト実行中にのみ使用されます。

エンジンがrequireされるときに依存gemもすぐにrequireしたい場合は、以下のようにエンジンが初期化されるより前にrequireする必要があることにご注意ください。たとえば次のようになります。

require 'other_engine/engine'
require 'yet_another_engine/engine'

module MyEngine
  class Engine < ::Rails::Engine
  end
end

7 Active Supportのon_loadフック

Ruby on RailsのActive Supportは、Ruby言語の拡張やユーティリティといったシステム横断的なユーティリティを提供するコンポーネントです。

Railsのコードは、アプリ読み込みの段階で参照されることがよくあります。Railsはこれらのフレームワークの読み込み順序について責任を持つため、途中でActiveRecord::Baseといったフレームワークを読み込んでしまうと、Railsがアプリに期待する暗黙の規約に違反してしまう可能性があります。さらに、ActiveRecord::Baseのコードをアプリ起動時に読み込んでしまうと、そうしたフレームワーク全体が再読み込みされるため、起動に時間がかかったり読み込み順序で競合が発生したりする可能性も生じます。

on_loadフックは、Railsの読み込み規約に違反しない形で初期化プロセスにフックをかけるAPIです。起動が遅くなる問題の軽減や、競合問題の回避にも利用できます。

8 on_loadフックとは何か

Rubyは動的言語であるため、あるコードが別のコードを読み込むことがあります。次のコードをご覧ください。

ActiveRecord::Base.include(MyActiveRecordHelper)

上のスニペットでは、このファイルの読み込み時にActiveRecord::Base行にさしかかります。Rubyはこのタイミングで定数の定義を探索し、それからrequireします。すなわち、Active Recordフレームワーク全体が起動時に読み込まれます。

ActiveSupport.on_loadは、あるコードの読み込みを、実際に必要になる時点まで遅延できるメカニズムです。上のスニペットは次のように書き換えられます。

ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper }

新しいスニペットは、ActiveRecord::Baseの読み込み時にMyActiveRecordHelperだけをincludeするようになります。

9 しくみ

Railsフレームワークにおけるこれらのフックは、特定のライブラリの読み込み時に呼び出されます。たとえば、ActionController::Baseが読み込まれると:action_controller_baseフックが呼び出されます。すなわち、:action_controller_baseフックでまとめられたすべてのActiveSupport.on_load呼び出しは、ActionController::Baseのコンテキストで呼び出される(ここではselfActionController::Baseとして評価される)ということです。

10 on_loadフックでコードを変更する

一般に、(フックによる)コードの変更方法は単純です。たとえば、ActiveRecord::Baseを参照するコードをon_loadフックで囲むことができます。

10.1 例1

ActiveRecord::Base.include(MyActiveRecordHelper)

上のコードは以下のように書けます。

ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } 
# selfがActiveRecord::Baseを指すので`#include`呼び出しが簡潔になる

10.2 例2

ActionController::Base.prepend(MyActionControllerHelper)

上のコードは以下のように書けます。

ActiveSupport.on_load(:action_controller_base) { prepend MyActionControllerHelper }
# selfがActiveRecord::Baseを指すので`#prepend`呼び出しが簡潔になる

10.3 例3

ActiveRecord::Base.include_root_in_json = true

上のコードは以下のように書けます。

ActiveSupport.on_load(:active_record) { self.include_root_in_json = true } 
# selfはActiveRecord::Baseを指す

11 利用可能なフック

利用可能なフックは以下のとおりです。

クラスの初期化プロセスをフックしたい場合は、以下のクラスに対応するフックを使います。

クラス 対応するフック
ActionCable action_cable
ActionController::API action_controller_api
ActionController::API action_controller
ActionController::Base action_controller_base
ActionController::Base action_controller
ActionController::TestCase action_controller_test_case
ActionDispatch::IntegrationTest action_dispatch_integration_test
ActionMailer::Base action_mailer
ActionMailer::TestCase action_mailer_test_case
ActionView::Base action_view
ActionView::TestCase action_view_test_case
ActiveJob::Base active_job
ActiveJob::TestCase active_job_test_case
ActiveRecord::Base active_record
ActiveSupport::TestCase active_support_test_case
i18n i18n

12 設定用フック

設定用のフックは以下のとおりです。設定用フックは特定のフレームワークにフックするのではなく、アプリケーション全体のコンテキストで実行されます。

フック ユースケース
before_configuration 最初に実行される設定フックです。あらゆる初期化より先に呼びされます。            
before_initialize   次に実行される設定フックです。フレームワークの初期化の直前で呼び出されます。              
before_eager_load   初期化後に実行される設定フックです。config.cache_classesがfalseの場合は実行されません。
after_initialize     最後に実行される設定フックです。 フレームワークの初期化後に呼び出しされます。                  

12.1 例

config.before_configuration { puts 'I am called before any initializers' }