Rails エンジン入門

本ガイドでは、Railsの「エンジン」について解説します。Railsエンジンのきわめて簡潔で使いやすいインターフェイスを用いて、ホストとなるRailsアプリケーションに機能を追加する方法についても解説します。

このガイドの内容:

  • エンジンの役割
  • エンジンの生成方法
  • エンジンのビルド方法
  • エンジンをアプリケーションにフックする
  • エンジン機能をアプリケーションで上書きする
  • 読み込み/設定フックでRailsフレームワークが読み込まれないようにする方法

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

エンジン (engine) は、ホストとなるRailsアプリケーションに機能を提供するミニチュア版Railsアプリケーションとみなせます。この場合、ホストとなる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オプションは、名前空間(namespace)で分離されたマウンタブルエンジンを生成する場合に使います。このジェネレータで生成したプラグインのスケルトン構造は、--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に置かれます。メーラーも同様に名前空間化されます。

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

2.1.2 appディレクトリ

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

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

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

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

app/controllersと同様に、app/helpersapp/jobsapp/mailersapp/modelsディレクトリの下にもそれぞれblorghというディレクトリがあり、その中にapplication_helper.rbというファイルがあります。このファイルは、エンジンのヘルパーで使うあらゆる共通機能を提供します。自分のファイルをこの名前空間化されたディレクトリの中に配置することで、他のエンジンに含まれる名前が完全に同一なルーティングヘルパーと衝突することも、アプリケーション内にあるヘルパーと衝突することも防止できます。

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

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

2.1.3 binディレクトリ

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

$ bin/rails generate model

もちろん、Engineクラスにisolate_namespaceを持つエンジンでは、このコマンドで生成したものがすべて名前空間化されることを理解しておきましょう。

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をはじめようと大して変わらない流れですが、多少の新味も加えられています。

本節のコマンドは、必ずblorghエンジンのルートディレクトリで実行してください。

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
create      app/views/blorgh/articles/_article.html.erb
invoke    resource_route
invoke    test_unit
create      test/controllers/blorgh/articles_controller_test.rb
create      test/system/blorgh/articles_test.rb
invoke    helper
create      app/helpers/blorgh/articles_helper.rb
invoke      test_unit

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リソースがあっても衝突を回避できます。

エンジンのルートディレクトリで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    test_unit

このフォームは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

マイグレーションでMIGRATIONS_PATHを指定することで、ソースのエンジンのカスタムパスを指定できます。

$ bin/rails railties:install:migrations MIGRATIONS_PATH=db_blourgh

マルチプルデータベースを利用している場合は、DATABASEを指定することでターゲットのデータベースを指定できます。

$ bin/rails railties:install:migrations DATABASE=animals

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

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:migrateを実行します。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 アプリケーションのモデルをエンジンで使う

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

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

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

$ bin/rails generate 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 generate 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/_article.html.erbの"Title"出力の上に追加します。

<p>
  <strong>Author:</strong>
  <%= 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

mattr_accessorメソッドの動作はattr_accessorcattr_accessorなどの姉妹メソッドと似ていますが、指定されたモジュールにゲッターメソッドとセッターメソッドを提供します(訳注: cattr_accessormattr_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 機能テスト

ここで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アプリケーション側でそれらのクラスを再オープン(再定義)することで拡張できます。

オーバーライドは専用のapp/overridesディレクトリに配置できます。これはオートローダーで無視され、 to_prepareコールバックでプリロードされます。

# config/application.rb
module MyApp
  class Application < Rails::Application
    # ...

    overrides = "#{Rails.root}/app/overrides"
    Rails.autoloaders.main.ignore(overrides)

    config.to_prepare do
      Dir.glob("#{overrides}/**/*_override.rb").sort.each do |override|
        load override
      end
    end
  end
end
6.1.1 既存のクラスをclass_evalで再オープンする

たとえば以下のエンジンモデルをオーバライドするとします。

# Blorgh/app/models/blorgh/article.rb
module Blorgh
  class Article < ApplicationRecord
    # ...
  end
end

これは、以下のようにクラスを「再オープン」するファイルを作成するだけでできます。

# MyApp/app/overrides/models/blorgh/article_override.rb
Blorgh::Article.class_eval do
  # ...
end

このオーバーライドが、そのクラスやモジュールを「再オープン」することがきわめて重要です。定義がエンジン内にあるので、クラスやモジュールがメモリ上にない状態でそれらをclassmoduleキーワードで定義すると、正しいものでなくなる可能性があります。上のようにclass_evalを使うことで、確実に再オープンできるようになります。

6.1.2 既存のクラスをActiveSupport::Concernで再オープンする

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/blorgh/article.rb
module Blorgh
  class Article < ApplicationRecord
    include Blorgh::Concerns::Models::Article
  end
end
# Blorgh/lib/concerns/models/article.rb

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

  # `included do`は、インクルードされたコードを
  # それがインクルードされているコンテキスト(つまりBlorgh::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の自動読み込みと再読み込み」ガイドの自動読み込みとRailsエンジンを参照してください。

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

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.4 ルーティング

デフォルトでは、エンジン内部のルーティングはアプリケーションのルーティングから分離されています。これは、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.5 アセット

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

エンジン内の他のコンポーネントと同様、アセットも名前空間化される必要があります。たとえば、style.cssというアセットは、app/assets/railsguides/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.6 アセットとプリコンパイルを分離する

エンジン内のアセットが、ホスト側のアプリケーションで必須ではない場合があります。 たとえば、エンジンでしか使わない管理機能を作成したとしましょう。この場合、これらのアセットはgemのadminレイアウトでしか使われないため、ホストアプリケーションでは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.7 他のgemとの依存関係

エンジンはgemとしてインストールされる可能性があるので、エンジン内のGemの依存関係は、エンジンのルートにある.gemspecファイル内で指定する必要があります。依存関係をGemfileで指定すると、従来のgemインストールでは認識されなくなって必要なgemがインストールされず、エンジンが誤動作する原因になります。

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

s.add_dependency "moo"

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

s.add_development_dependency "moo"

どちらの依存gemも、アプリケーションでbundle installを実行するときにインストールされます。gemの開発用依存関係は、エンジンをdevelopment環境またはtest環境で実行する場合にのみ使われます。

エンジンがrequireされたタイミングで依存gemもすぐにrequireしたい場合は、以下のようにエンジンが初期化されるより前にrequireする必要があるので注意が必要です。

require "other_engine/engine"
require "yet_another_engine/engine"

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

7 フックの読み込みと設定

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

読み込みフックと設定フックは、Railsの読み込み規約に違反せずにこの初期化プロセスにフックできるAPIです。また、起動パフォーマンス低下も緩和され、コンフリクトも回避されます。

7.1 Railsフレームワークの読み込みを回避する

Rubyは動的言語であるため、一部のコードで別のRailsフレームワークが読み込まれることがあります。次のコードをご覧ください

ActiveRecord::Base.include(MyActiveRecordHelper)

上のスニペットの動作は次のようになります。Rubyがこのファイルを読み込んでActiveRecord::Baseまで進むと、それをきっかけに定数の定義を探索してrequireします。このようにして、Active Recordフレームワーク全体が起動時に読み込まれます。

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

ActiveSupport.on_load(:active_record) do
  include MyActiveRecordHelper
end

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

7.2 フックが呼び出されるタイミング

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

7.3 読み込みフックでコードを変更する

一般に、(フックによる)コードの変更方法は単純です。たとえば、ActiveRecord::BaseなどのRailsフレームワークを参照するコードは、読み込みフックでラップできます。

include呼び出しを変更する場合

ActiveRecord::Base.include(MyActiveRecordHelper)

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

ActiveSupport.on_load(:active_record) do
  # selfがActiveRecord::Baseを指すので
  # includeを呼び出せる
  include MyActiveRecordHelper
end

prepend呼び出しを変更する場合

ActionController::Base.prepend(MyActionControllerHelper)

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

ActiveSupport.on_load(:action_controller_base) do
  # selfがActiveRecord::Baseを指すので
  # includeを呼び出せる
  prepend MyActionControllerHelper
end

クラスメソッド呼び出しを変更する場合

ActiveRecord::Base.include_root_in_json = true

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

ActiveSupport.on_load(:active_record) do
  # ここではselfがActiveRecord::Baseを指す
  self.include_root_in_json = true
end

8 利用可能なフック

これらの読み込みフックは、自分のコードで利用できます。以下のクラスの初期化プロセスにフックするには、利用可能なフックを使います。

クラス 対応するフック
ActionCable action_cable
ActionCable::Channel::Base action_cable_channel
ActionCable::Connection::Base action_cable_connection
ActionCable::Connection::TestCase action_cable_connection_test_case
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
ActionDispatch::Response action_dispatch_response
ActionDispatch::Request action_dispatch_request
ActionDispatch::SystemTestCase action_dispatch_system_test_case
ActionMailbox::Base action_mailbox
ActionMailbox::InboundEmail action_mailbox_inbound_email
ActionMailbox::Record action_mailbox_record
ActionMailbox::TestCase action_mailbox_test_case
ActionMailer::Base action_mailer
ActionMailer::TestCase action_mailer_test_case
ActionText::Content action_text_content
ActionText::Record action_text_record
ActionText::RichText action_text_rich_text
ActionText::EncryptedRichText action_text_encrypted_rich_text
ActionView::Base action_view
ActionView::TestCase action_view_test_case
ActiveJob::Base active_job
ActiveJob::TestCase active_job_test_case
ActiveRecord::Base active_record
ActiveRecord::TestFixtures active_record_fixtures
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter active_record_postgresqladapter
ActiveRecord::ConnectionAdapters::Mysql2Adapter active_record_mysql2adapter
ActiveRecord::ConnectionAdapters::TrilogyAdapter active_record_trilogyadapter
ActiveRecord::ConnectionAdapters::SQLite3Adapter active_record_sqlite3adapter
ActiveStorage::Attachment active_storage_attachment
ActiveStorage::VariantRecord active_storage_variant_record
ActiveStorage::Blob active_storage_blob
ActiveStorage::Record active_storage_record
ActiveSupport::TestCase active_support_test_case
i18n i18n

9 設定用フック

設定用フックは特定のフレームワークにはフックせず、アプリケーション全体のコンテキストで実行されます。

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

設定フックは、Engineクラス内で呼び出されます。

module Blorgh
  class Engine < ::Rails::Engine
    config.before_configuration do
      puts 'I am called before any initializers'
    end
  end
end

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加