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 (親アプリケーションに認証機能を提供するエンジン) や Forem (フォーラム機能を提供するエンジン) を参照してください。この他に、Spree (eコマースプラットフォーム) やRefineryCMS (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
ディレクトリの中には、通常のアプリケーションでおなじみの標準のassets
、controllers
、helpers
、mailers
、models
、views
ディレクトリが置かれます。このうちhelpers
、mailers
、models
ディレクトリにはデフォルトでは何も置かれないので、本セクションでは解説しません。モデルについては、エンジンの作成について解説するセクションで後述します。
エンジンのapp/assets
ディレクトリの下にも、通常のアプリケーションと同様にimages
、javascripts
、stylesheets
ディレクトリがそれぞれあります。通常のアプリケーションと異なる点は、これらのディレクトリの下にはさらにエンジン名を持つサブディレクトリがあることです。これは、エンジンが名前空間化されるのと同様、エンジンのアセットも同様に名前空間化される必要があるからです。
app/controllers
ディレクトリの下にはblorgh
ディレクトリが置かれます。この中にはapplication_controller.rb
というファイルが1つ置かれます。このファイルはエンジンのコントローラ共通の機能を提供するためのものです。このblorgh
ディレクトリには、エンジンで使用するその他のコントローラを置きます。これらのファイルを名前空間化されたディレクトリに配置することで、他のエンジンやアプリケーションに同じ名前のコントローラがあっても名前の衝突を避ける事ができます。
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
を使ってクラスが読み込まれるかどうかを保証するのが正しい使い方です。
あるエンジンに含まれるApplicationController
というクラスの名前は、アプリケーションそのものが持つクラスと同じ名前になっています。これは、アプリケーションをエンジンに変換しやすくするためです。
最後に、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 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_controller.rb
) も同時に生成します。
このジェネレータによって生成されるものはすべて正しく名前空間化されます。このコントローラのクラスは、以下のようにBlorgh
モジュール内で定義されます。
module Blorgh class ArticlesController < ApplicationController ... end end
このクラスで継承されているApplicationController
クラスは、実際にはApplicationController
ではなく、Blorgh::ApplicationController
です。
app/helpers/blorgh/articles_helper.rb
のヘルパーも同様に名前空間化されます。
module Blorgh module ArticlesHelper ... end end
これにより、たとえ他のエンジンやアプリケーションにarticleリソースがあっても衝突を回避できます。
最後に、以下の2つのファイルがこのリソースのアセットとして生成されます。
app/assets/javascripts/blorgh/articles.js
と
app/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_many
はBlorgh
モジュールの中にあるクラスの中で定義されています。これだけで、これらのオブジェクトに対してBlorgh::Comment
モデルを使用したいという意図がRailsに自動的に認識されます。従って、ここで:class_name
オプションを使用してクラス名を指定する必要はありません。
続いて、記事を作成するためのフォームを作成する必要があります。フォームを追加するには、app/views/blorgh/articles/show.html.erb
のrender @article.comments
呼び出しの直後に以下の行を追加します。
<%= render "blorgh/comments/form" %>
続いて、この行を出力に含めるためのパーシャル (部分テンプレート) も必要です。app/views/blorgh/comments
にディレクトリを作成し、_form.html.erb
というファイルを作成します。このファイルの中に以下のパーシャルを記述します。
<h3>New comment</h3> <%= form_for [@article, @article.comments.build] do |f| %> <p> <%= f.label :text %><br> <%= f.text_area :text %> </p> <%= f.submit %> <% end %>
このフォームが送信されると、エンジン内の/articles/:article_id/comments
というルーティングに対してPOST
リクエストを送信しようとします。このルーティングはまだ存在していませんので、config/routes.rb
のresources :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::CommentsController
のcreate
アクションです。このアクションを作成する必要があります。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: "/path/to/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
テーブル用のマイグレーションが含まれます。これらのテーブルをアプリケーションのデータベースに作成し、エンジンのモデルからこれらのテーブルにアクセスできるようにする必要があります。これらのマイグレーションをアプリケーションにコピーするには、以下のコマンドを実行します。
$ 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"> <%= f.label :author_name %><br> <%= f.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_accessor
やcattr_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.rb
のBlorgh
モジュール内部の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::Article
のbelongs_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 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.rb
のEngine
クラスによって構成されます。
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.css
やadmin.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.css admin.js) 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
Active SupportはRuby言語の拡張、ユーティリティとそれ以外の巡回用ユーティリティを提供する役割を担っているRuby on Railsのコンポーネントです。
Railsのコードは、アプリケーション読み込みの時点でよく参照されます。Railsはこれらのフレームワークの読み込み順番に関与しているため、例えばActiveRecord::Base
フレームワークを途中で読み込んで、アプリケーションがRailsと交わした暗黙的な約束に違反してしまう可能性があります。ActiveRecord::Base
のコードをアプリケーション起動時に読み込んだ場合はそれだけでなく、全体のフレームワークを再度読み込むことになってしまうので、起動時間が長くなったり読み込み順序に衝突が起きる可能性もあります。
ロードフックは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
のコンテキストで呼び出されるということです。言い換えると、self
がActionController::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' }