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に置かれます。メイラーも同様に名前空間化されます。

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

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/helpersディレクトリの下にはblorghというディレクトリがあり、その中にapplication_helper.rbというファイルがあります。このファイルは、エンジンのヘルパーで使うあらゆる共通機能を提供します。blorghディレクトリは、エンジンが使うその他のヘルパーの置き場所です。この名前空間化されたディレクトリの中に配置することで、他のエンジンに含まれる名前が完全に同一なルーティングヘルパーと衝突することも、アプリケーション内にあるヘルパーと衝突することも防止できます。

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

app/mailersディレクトリの下にはblorghというディレクトリがあり、その中にapplication_ mailer.rbというファイルがあります。このファイルは、エンジンのメイラーで使うあらゆる共通機能を提供します。blorghディレクトリは、エンジンが使うその他のメイラーの置き場所です。この名前空間化されたディレクトリの中に配置することで、他のエンジンに含まれる名前が完全に同一なメイラーと衝突することも、アプリケーション内にあるメイラーと衝突することも防止できます。

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

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

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

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アプリケーション側でそれらのクラスを再オープン(再定義)することで拡張できます。Railsのモデルクラスとコントローラクラスは、Rails特有の機能を継承しているほかは通常のRubyクラスと変わりありません。オープンクラスの手法を使えば、エンジンのクラスをメインのアプリケーションで使えるように再定義されます。これは、デザインパターンで言うDecoratorパターンとして実装するのが普通です(訳注: 本セクションでのDecoratorパターンへの言及は適切ではないということでRails 6のガイドでは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 アセットとプリコンパイルを分離する

エンジン内のアセットは、ホスト側のアプリケーションでは必要ではないことがあります。たとえば、エンジンでしか使わない管理機能を作成したとしましょう。この場合、これらのアセットはgemのadminレイアウトでしか使われないため、ホストアプリケーションではadmin.cssadmin.jsは不要です。ホストアプリケーションから見れば、自分が持つスタイルシートに"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としてインストールされるので、エンジンが依存する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は、エンジンのテスト実行中にのみ利用されます。

エンジンが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がActionController::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
ActionDispatch::SystemTestCase action_dispatch_system_test_case
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.eager_loadがfalseの場合は実行されません。
after_initialize     最後に実行される設定フックです。 フレームワークの初期化後に呼び出しされます。                  

12.1 例

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

支援・協賛

Railsガイドは下記のサポーターから継続的な支援を受けています。