Rails ジェネレータとテンプレート入門

Railsの各種ジェネレータは、ワークフローの改善に欠かせないツールです。本ガイドは、Railsジェネレータの作成方法および既存のジェネレータのカスタマイズ方法について解説します。

このガイドの内容:

  • アプリケーションで利用できるジェネレータを確認する方法
  • テンプレートでジェネレータを作成する方法
  • Railsがジェネレータを起動前に探索する方法
  • ジェネレータのテンプレートをオーバーライドしてscaffoldをカスタマイズする方法
  • ジェネレータをオーバーライドしてscaffoldをカスタマイズする方法
  • 多数のジェネレータを誤って上書きしないためのフォールバック方法
  • アプリケーションテンプレートの作成方法

1 ジェネレータとの最初の出会い

railsコマンドでRailsアプリケーションを作成すると、実はRailsのジェネレータを利用したことになります。以後は、bin/rails generateを実行すれば、その時点でアプリケーションから利用可能なすべてのジェネレータのリストが表示されます。

$ rails new myapp
$ cd myapp
$ bin/rails generate

Railsアプリケーションを新しく作成するときは、gem install railsでインストールしたrails gemのグローバルなrailsコマンドを使いますが、作成したアプリケーションのディレクトリ内では、そのアプリケーション内にバンドルされているbin/railsコマンドを使う点が異なります。

Railsで利用可能なすべてのジェネレータのリストを表示できます。特定のジェネレータのヘルプを表示するには、そのジェネレータ名に続けて以下のように--helpオプションを指定します。

$ bin/rails generate scaffold --help

2 最初のジェネレータを作成する

ジェネレータはThor gemの上に構築されています。Thorは強力な解析オプションと優れたファイル操作APIを提供しています。

具体例として、config/initializersディレクトリの下にinitializer.rbという名前のイニシャライザファイルを作成するジェネレータを構築してみましょう。

class InitializerGenerator < Rails::Generators::Base
  def create_initializer_file
    create_file "config/initializers/initializer.rb", <<~RUBY
      # 初期化時のコンテンツをここに追加する
    RUBY
  end
end

新しいジェネレータはきわめてシンプルです。Rails::Generators::Baseを継承しており、定義されているメソッドは1つだけです。ジェネレータが起動されると、ジェネレータ内で定義されているパブリックメソッドが定義順に実行されます。作成したメソッドからcreate_fileが呼び出され、指定の内容を含むファイルが指定のディレクトリに作成されます。

新しいジェネレータを呼び出すには、以下を実行します。

$ bin/rails generate initializer

次に進む前に、今作成したばかりのジェネレータの説明を表示してみましょう。

$ bin/rails generate initializer --help

Railsでは、ジェネレータがActiveRecord::Generators::ModelGeneratorのように名前空間化されていれば実用的な説明文を生成できますが、今作成したジェネレータはそうなっていません。この問題は2通りの方法で解決できます。1つ目の方法は、ジェネレータ内でdescメソッドを呼び出すことです。

class InitializerGenerator < Rails::Generators::Base
  desc "このジェネレータはconfig/initializersにイニシャライザファイルを作成します"
  def create_initializer_file
    create_file "config/initializers/initializer.rb", <<~RUBY
      # 初期化時のコンテンツをここに追加する
    RUBY
  end
end

これで、--helpを付けて新しいジェネレータを呼び出すと新しい説明文が表示されるようになりました。

説明文を追加する2つ目の方法は、ジェネレータと同じディレクトリにUSAGEという名前のファイルを作成することです。次に、この方法で実際に説明文を追加してみましょう。

3 ジェネレータでジェネレータを生成する

Railsには、ジェネレータを生成するためのジェネレータもあります。InitializerGeneratorを削除してから、bin/rails generate generatorを実行して新しいジェネレータを生成してみましょう。

$ rm lib/generators/initializer_generator.rb

$ bin/rails generate generator initializer
      create  lib/generators/initializer
      create  lib/generators/initializer/initializer_generator.rb
      create  lib/generators/initializer/USAGE
      create  lib/generators/initializer/templates
      invoke  test_unit
      create    test/lib/generators/initializer_generator_test.rb

上で作成したジェネレータの内容は以下のとおりです。

class InitializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)
end

上のジェネレータを見て最初に気付く点は、Rails::Generators::BaseではなくRails::Generators::NamedBaseを継承していることです。これは、このジェネレータを生成するには引数が1つ以上必要であることを意味します。この引数はイニシャライザ名で、コードはこのイニシャライザ名をnameという変数で参照できます。

新しいジェネレータを呼び出すと、以下のように説明文が表示されます。

$ bin/rails generate initializer --help
Usage:
  bin/rails generate initializer NAME [options]

次に、新しいジェネレータにはsource_rootという名前のクラスメソッドが含まれている点にもご注目ください。このメソッドは、ジェネレータのテンプレートの置き場所を指定する場合に使います。デフォルトでは、作成されたlib/generators/initializer/templatesディレクトリを指します。

ジェネレータのテンプレートの機能を理解するために、lib/generators/initializer/templates/initializer.rbを作成して以下のコンテンツを追加してみましょう。

# 初期化用のコンテンツをここに追加する

続いてジェネレータを変更し、呼び出されたときにこのテンプレートをコピーするようにします。

class InitializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  def copy_initializer_file
    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
  end
end

それではこのジェネレータを実行してみましょう。

$ bin/rails generate initializer core_extensions
      create  config/initializers/core_extensions.rb

$ cat config/initializers/core_extensions.rb
# 初期化用のコンテンツをここに追加する

copy_fileが作成したconfig/initializers/core_extensions.rbファイルにテンプレートのコンテンツが反映されていることがわかります(コピー先パスで使われるfile_nameメソッドはRails::Generators::NamedBaseから継承されます)。

4 ジェネレータのコマンドラインオプション

ジェネレータでは、以下のようにclass_optionでコマンドラインオプションをサポートできます。

class InitializerGenerator < Rails::Generators::NamedBase
  class_option :scope, type: :string, default: "app"
end

これで、--scopeオプションを指定してジェネレータを呼び出せるようになります。

$ bin/rails generate initializer theme --scope dashboard

ジェネレータ内では、optionsでオプションの値を参照できます。

def copy_initializer_file
  @scope = options["scope"]
end

5 ジェネレータ名の解決

Railsがジェネレータ名を解決するときは、複数のファイル名を使ってジェネレータを探索します。たとえば、bin/rails generate initializer core_extensionsを実行すると、Railsはジェネレータが見つかるまで以下の順にファイルを探索します。

  • rails/generators/initializer/initializer_generator.rb
  • generators/initializer/initializer_generator.rb
  • rails/generators/initializer_generator.rb
  • generators/initializer_generator.rb

ジェネレータがどのファイルにも見つからない場合は、エラーメッセージが表示されます。

上の例でアプリケーションのlib/ディレクトリの下にファイルを置いているのは、このディレクトリが$LOAD_PATHに含まれているからです。これにより、Railsがこのファイルを検索して読み込めるようになります。

6 Railsジェネレータのテンプレートをオーバーライドする

Railsは、ジェネレータのテンプレートファイルを解決するときにも複数の場所を探索します。アプリケーションのlib/templates/ディレクトリも探索場所の1つです。この振る舞いのおかげで、Railsの組み込みジェネレータで使われるテンプレートをオーバーライドできます。たとえば、コントローラのscaffoldテンプレートビューのscaffoldテンプレートをオーバーライドできます。

これを実際に行うために、lib/templates/erb/scaffold/index.html.erb.ttファイルを作成して以下のコンテンツを追加してみましょう。

<%% @<%= plural_table_name %>.count %> <%= human_name.pluralize %>

ここで作成するERBテンプレートは、別のERBテンプレートをレンダリングします。そのため、生成されるテンプレートに出力する<%は、ジェネレータのテンプレートで<%%のようにすべてエスケープしておく必要がある点にご注意ください。

それでは、Rails組み込みのscaffoldジェネレータを実行してみましょう。

$ bin/rails generate scaffold Post title:string
      ...
      create      app/views/posts/index.html.erb
      ...

app/views/posts/index.html.erbファイルを開くと、以下のようになっているはずです。

<% @posts.count %> Posts

7 Railsジェネレータをオーバーライドする

Rails組み込みのジェネレータは、config.generatorsで設定できます。一部のジェネレータについては完全にオーバーライドすることも可能です。

まず、scaffoldジェネレータの動作をじっくり見てみましょう。

$ bin/rails generate scaffold User name:string
      invoke  active_record
      create    db/migrate/20230518000000_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      create      app/views/users/_user.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      create      test/system/users_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder

この出力結果を見ると、scaffoldジェネレータが別のジェネレータ(scaffold_controllerなど)を実行していることがわかります。また、一部のジェネレータはさらに別のジェネレータを実行しています。特に、scaffold_controllerジェネレータはhelperジェネレータなど多くのジェネレータを実行しています。

組み込みのhelperジェネレータを新しいジェネレータでオーバーライドしてみましょう。新しいジェネレータの名前はmy_helperにします。

$ bin/rails generate generator rails/my_helper
      create  lib/generators/rails/my_helper
      create  lib/generators/rails/my_helper/my_helper_generator.rb
      create  lib/generators/rails/my_helper/USAGE
      create  lib/generators/rails/my_helper/templates
      invoke  test_unit
      create    test/lib/generators/rails/my_helper_generator_test.rb

次に、lib/generators/rails/my_helper/my_helper_generator.rbファイルで以下のジェネレータを定義します。

class Rails::MyHelperGenerator < Rails::Generators::NamedBase
  def create_helper_file
    create_file "app/helpers/#{file_name}_helper.rb", <<~RUBY
      module #{class_name}Helper
        # 私はヘルパー
      end
    RUBY
  end
end

最後に、組み込みのhelperジェネレータではなくmy_helperジェネレータを使うようRailsに指示する必要があります。これにはconfig.generators設定を使います。config/application.rbファイルに以下を追加しましょう。

config.generators do |g|
  g.helper :my_helper
end

これで、scaffoldジェネレータをもう一度実行すると、my_helperジェネレータが動作していることがわかります。

$ bin/rails generate scaffold Article body:text
      ...
      invoke  scaffold_controller
      ...
      invoke    my_helper
      create      app/helpers/articles_helper.rb
      ...

組み込みのhelperジェネレータにはinvoke test_unitという行がありますが、今作ったmy_helperジェネレータにはありません。helperジェネレータはデフォルトではテストを生成しませんが、hook_forでテストを生成するためのフックを提供しています。MyHelperGeneratorクラスにhook_for :test_framework, as: :helperを追加すれば、これと同じことを実現できます。詳しくはhook_forのドキュメントを参照してください。

7.1 ジェネレータのフォールバック

特定のジェネレータをオーバーライドする別の方法は、フォールバックを使う方法です。フォールバックを使うと、あるジェネレータの名前空間を別のジェネレータの名前空間に委譲できます。

たとえば、my_test_unit:modelジェネレータを作成してtest_unit:modelジェネレータをオーバーライドしたいとします。しかし、test_unit:controllerジェネレータなどの他のtest_unit:*ジェネレータはオーバーライドしたくないとします。

最初に、my_test_unit:modelジェネレータをlib/generators/my_test_unit/model/model_generator.rbファイルに作成します。

module MyTestUnit
  class ModelGenerator < Rails::Generators::NamedBase
    source_root File.expand_path("templates", __dir__)

    def do_different_stuff
      say "別の作業を実行中..."
    end
  end
end

次に、config.generators設定を変更してtest_frameworkジェネレータをmy_test_unitに設定します。さらに、my_test_unit:*ジェネレータが見つからない場合はtest_unit:*ジェネレータに解決するフォールバックも設定します。

config.generators do |g|
  g.test_framework :my_test_unit, fixture: false
  g.fallbacks[:my_test_unit] = :test_unit
end

これで、scaffoldジェネレータを実行すると、my_test_unitジェネレータがtest_unitジェネレータに置き換わり、モデルのテスト以外は影響を受けていないことがわかります。

$ bin/rails generate scaffold Comment body:text
      invoke  active_record
      create    db/migrate/20230518000000_create_comments.rb
      create    app/models/comment.rb
      invoke    my_test_unit
    Doing different stuff...
      invoke  resource_route
       route    resources :comments
      invoke  scaffold_controller
      create    app/controllers/comments_controller.rb
      invoke    erb
      create      app/views/comments
      create      app/views/comments/index.html.erb
      create      app/views/comments/edit.html.erb
      create      app/views/comments/show.html.erb
      create      app/views/comments/new.html.erb
      create      app/views/comments/_form.html.erb
      create      app/views/comments/_comment.html.erb
      invoke    resource_route
      invoke    my_test_unit
      create      test/controllers/comments_controller_test.rb
      create      test/system/comments_test.rb
      invoke    helper
      create      app/helpers/comments_helper.rb
      invoke      my_test_unit
      invoke    jbuilder
      create      app/views/comments/index.json.jbuilder
      create      app/views/comments/show.json.jbuilder

8 アプリケーションテンプレート

アプリケーションテンプレートは特殊なジェネレータです。このテンプレートでは、ジェネレータのヘルパーメソッドをすべて利用可能ですが、RubyクラスではなくRubyスクリプトとして記述する点が異なります。以下はアプリケーションテンプレートの例です。

# template.rb

if yes?("Deviseをインストールしますか?")
  gem "devise"
  devise_model = ask("ユーザモデル名は何にしますか?", default: "User")
end

after_bundle do
  if devise_model
    generate "devise:install"
    generate "devise", devise_model
    rails_command "db:migrate"
  end

  git add: ".", commit: %(-m 'Initial commit')
end

このテンプレートでは、最初にDevise gemをインストールするかどうかをユーザーに尋ねます。ユーザーが「yes」(または「y」)を入力すると、テンプレートはGemfileにDeviseを追加し、Deviseのユーザーモデル名をユーザーに尋ねます(デフォルトはUser)。その後、bundle installが実行された後、Deviseモデルが指定されている場合はDeviseジェネレータとrails db:migrateを実行します。最後に、テンプレートはアプリケーションディレクトリ全体に対してgit addgit commitを実行します。

新しいRailsアプリケーションをrails newコマンドで生成するときに-mオプションを渡すことで、このテンプレートを実行できます。

$ rails new my_cool_app -m path/to/template.rb

また、既存のアプリケーション内でbin/rails app:templateでテンプレートを実行することも可能です。

$ bin/rails app:template LOCATION=path/to/template.rb

テンプレートは必ずしもローカルに保存する必要はありません。パスの代わりにURLも指定できます。

$ rails new my_cool_app -m http://example.com/template.rb
$ bin/rails app:template LOCATION=http://example.com/template.rb

9 ジェネレータのヘルパーメソッド

Thorは、以下のような多くのヘルパーメソッドをThor::Actionsでジェネレータに提供しています。

また、RailsもRails::Generators::Actionsで以下のような多くのヘルパーメソッドを提供しています。

10 ジェネレータをテストする

Railsは、Rails::Generators::Testing::Behaviourで以下のようなテストヘルパーメソッドを提供しています。

ジェネレータに対してテストを実行する場合、デバッグツールが機能するために以下のようにコマンドでRAILS_LOG_TO_STDOUT=trueを指定する必要があります。

RAILS_LOG_TO_STDOUT=true ./bin/test test/generators/actions_test.rb

Railsではその他にも、Rails::Generators::Testing::Assertionsで追加のアサーションを提供しています。

フィードバックについて

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

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

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

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

支援・協賛

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

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